Skip to content

Commit b446120

Browse files
georgiy-belyaninTotktonada
authored andcommitted
Migrate cluster module and adapt it
The original `cluster` module (tarantool/test/config-luatest/cluster.lua) has been moved to the current project and will be available as follows: ```lua local t = require('luatest') local cluster = t.cluster:new(...) cluster:start() ``` It is used to simplify managing Tarantool clusters based on the provided configuration. The helper requires Tarantool 3.0.0 or newer. Otherwise cluster methods cause an error. Original helper created by: [email protected] Test author: [email protected] Closes #368
1 parent 7dc5cb7 commit b446120

File tree

5 files changed

+619
-1
lines changed

5 files changed

+619
-1
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
- Fix error trace reporting for functions executed with `Server:exec()`
2424
(gh-396).
2525
- Remove pretty-printing of `luatest.log` arguments.
26+
- Add `cluster` helper as a tool for managing a Tarantool cluster (gh-368).
2627

2728
## 1.0.1
2829

config.ld

+2-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ file = {
1010
'luatest/justrun.lua',
1111
'luatest/cbuilder.lua',
1212
'luatest/hooks.lua',
13-
'luatest/treegen.lua'
13+
'luatest/treegen.lua',
14+
'luatest/cluster.lua'
1415
}
1516
topics = {
1617
'CHANGELOG.md',

luatest/cluster.lua

+355
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,355 @@
1+
--- Tarantool 3.0+ cluster management utils.
2+
--
3+
-- The helper is used to automatically collect a set of
4+
-- instances from the provided configuration and automatically
5+
-- set up servers per each configured instance.
6+
--
7+
-- @usage
8+
--
9+
-- local cluster = Cluster:new(config)
10+
-- cluster:start()
11+
-- cluster['instance-001']:exec(<...>)
12+
-- cluster:each(function(server)
13+
-- server:exec(<...>)
14+
-- end)
15+
--
16+
-- After setting up a cluster object the following methods could
17+
-- be used to interact with it:
18+
--
19+
-- * :start() Startup the cluster.
20+
-- * :start_instance() Startup a specific instance.
21+
-- * :stop() Stop the cluster.
22+
-- * :each() Execute a function on each instance.
23+
-- * :size() get an amount of instances
24+
-- * :drop() Drop the cluster.
25+
-- * :sync() Sync the configuration and collect a new set of
26+
-- instances
27+
-- * :reload() Reload the configuration.
28+
--
29+
-- The module can also be used for testing failure startup
30+
-- cases:
31+
--
32+
-- Cluster:startup_error(config, error_message)
33+
--
34+
-- @module luatest.cluster
35+
36+
local fun = require('fun')
37+
local yaml = require('yaml')
38+
local assertions = require('luatest.assertions')
39+
local helpers = require('luatest.helpers')
40+
local hooks = require('luatest.hooks')
41+
local treegen = require('luatest.treegen')
42+
local justrun = require('luatest.justrun')
43+
local server = require('luatest.server')
44+
45+
local Cluster = require('luatest.class').new()
46+
47+
-- Cluster uses custom __index implementation to support
48+
-- getting instances from it using `cluster['i-001']`.
49+
--
50+
-- Thus, we need to change the metatable of the class
51+
-- with a custom __index method.
52+
local mt = getmetatable(Cluster)
53+
mt.__index = function(self, k)
54+
local method = rawget(mt, k)
55+
if method ~= nil then
56+
return method
57+
end
58+
59+
local server_map = rawget(self, '_server_map')
60+
if server_map ~= nil and server_map[k] ~= nil then
61+
return server_map[k]
62+
end
63+
64+
return rawget(self, k)
65+
end
66+
67+
local cluster = {
68+
_group = {}
69+
}
70+
71+
function Cluster:inherit(object)
72+
setmetatable(object, self)
73+
self.__index = self
74+
return object
75+
end
76+
77+
local function init(g)
78+
cluster._group = g
79+
end
80+
81+
-- Stop all the managed instances using <server>:drop().
82+
local function drop(g)
83+
if g._cluster ~= nil then
84+
g._cluster:drop()
85+
end
86+
g._cluster = nil
87+
end
88+
89+
local function clean(g)
90+
assert(g._cluster == nil)
91+
end
92+
93+
-- {{{ Helpers
94+
95+
-- Collect names of all the instances defined in the config
96+
-- in the alphabetical order.
97+
local function instance_names_from_config(config)
98+
local instance_names = {}
99+
for _, group in pairs(config.groups or {}) do
100+
for _, replicaset in pairs(group.replicasets or {}) do
101+
for name, _ in pairs(replicaset.instances or {}) do
102+
table.insert(instance_names, name)
103+
end
104+
end
105+
end
106+
table.sort(instance_names)
107+
return instance_names
108+
end
109+
110+
-- }}} Helpers
111+
112+
-- {{{ Cluster management
113+
114+
--- Execute for server in the cluster.
115+
--
116+
-- @func f Function to execute with a server as the first param.
117+
function Cluster:each(f)
118+
fun.iter(self._servers):each(function(iserver)
119+
f(iserver)
120+
end)
121+
end
122+
123+
--- Get cluster size.
124+
--
125+
-- @return number.
126+
function Cluster:size()
127+
return #self._servers
128+
end
129+
130+
--- Start all the instances.
131+
--
132+
-- @tab[opt] opts Cluster startup options.
133+
-- @bool[opt] opts.wait_until_ready Wait until servers are ready
134+
-- (default: false).
135+
function Cluster:start(opts)
136+
self:each(function(iserver)
137+
iserver:start({wait_until_ready = false})
138+
end)
139+
140+
-- wait_until_ready is true by default.
141+
local wait_until_ready = true
142+
if opts ~= nil and opts.wait_until_ready ~= nil then
143+
wait_until_ready = opts.wait_until_ready
144+
end
145+
146+
if wait_until_ready then
147+
self:each(function(iserver)
148+
iserver:wait_until_ready()
149+
end)
150+
end
151+
152+
-- wait_until_running is equal to wait_until_ready by default.
153+
local wait_until_running = wait_until_ready
154+
if opts ~= nil and opts.wait_until_running ~= nil then
155+
wait_until_running = opts.wait_until_running
156+
end
157+
158+
if wait_until_running then
159+
self:each(function(iserver)
160+
helpers.retrying({timeout = 60}, function()
161+
assertions.assert_equals(iserver:eval('return box.info.status'),
162+
'running')
163+
end)
164+
165+
end)
166+
end
167+
end
168+
169+
--- Start the given instance.
170+
--
171+
-- @string instance_name Instance name.
172+
function Cluster:start_instance(instance_name)
173+
local iserver = self._server_map[instance_name]
174+
assert(iserver ~= nil)
175+
iserver:start()
176+
end
177+
178+
--- Stop the whole cluster.
179+
function Cluster:stop()
180+
for _, iserver in ipairs(self._servers or {}) do
181+
iserver:stop()
182+
end
183+
end
184+
185+
--- Drop the cluster's servers.
186+
function Cluster:drop()
187+
for _, iserver in ipairs(self._servers or {}) do
188+
iserver:drop()
189+
end
190+
self._servers = nil
191+
self._server_map = nil
192+
end
193+
194+
--- Sync the cluster object with the new config.
195+
--
196+
-- It performs the following actions.
197+
--
198+
-- * Write the new config into the config file.
199+
-- * Update the internal list of instances.
200+
--
201+
-- @tab config New config.
202+
function Cluster:sync(config)
203+
assert(type(config) == 'table')
204+
205+
local instance_names = instance_names_from_config(config)
206+
207+
treegen.write_file(self._dir, self._config_file_rel, yaml.encode(config))
208+
209+
for i, name in ipairs(instance_names) do
210+
if self._server_map[name] == nil then
211+
local iserver = server:new(fun.chain(self._server_opts, {
212+
alias = name,
213+
}):tomap())
214+
table.insert(self._servers, i, iserver)
215+
self._server_map[name] = iserver
216+
end
217+
end
218+
219+
end
220+
221+
--- Reload configuration on all the instances.
222+
--
223+
-- @tab[opt] config New config.
224+
function Cluster:reload(config)
225+
assert(config == nil or type(config) == 'table')
226+
227+
-- Rewrite the configuration file if a new config is provided.
228+
if config ~= nil then
229+
treegen.write_file(self._dir, self._config_file_rel,
230+
yaml.encode(config))
231+
end
232+
233+
-- Reload config on all the instances.
234+
self:each(function(iserver)
235+
-- Assume that all the instances are started.
236+
--
237+
-- This requirement may be relaxed if needed, it is just
238+
-- for simplicity.
239+
assert(iserver.process ~= nil)
240+
241+
iserver:exec(function()
242+
local cfg = require('config')
243+
244+
cfg:reload()
245+
end)
246+
end)
247+
end
248+
249+
--- Create a new Tarantool cluster.
250+
--
251+
-- @tab config Cluster configuration.
252+
-- @tab[opt] server_opts Extra options passed to server:new().
253+
-- @tab[opt] opts Cluster options.
254+
-- @string[opt] opts.dir Specific directory for the cluster.
255+
-- @return table
256+
function Cluster:new(config, server_opts, opts)
257+
local g = cluster._group
258+
259+
assert(type(config) == 'table')
260+
assert(config._config == nil, "Please provide cbuilder:new():config()")
261+
assert(g._cluster == nil)
262+
263+
-- Prepare a temporary directory and write a configuration
264+
-- file.
265+
local dir = opts and opts.dir or treegen.prepare_directory({}, {})
266+
local config_file_rel = 'config.yaml'
267+
local config_file = treegen.write_file(dir, config_file_rel,
268+
yaml.encode(config))
269+
270+
-- Collect names of all the instances defined in the config
271+
-- in the alphabetical order.
272+
local instance_names = instance_names_from_config(config)
273+
274+
assert(next(instance_names) ~= nil, 'No instances in the supplied config')
275+
276+
-- Generate luatest server options.
277+
server_opts = fun.chain({
278+
config_file = config_file,
279+
chdir = dir,
280+
net_box_credentials = {
281+
user = 'client',
282+
password = 'secret',
283+
},
284+
}, server_opts or {}):tomap()
285+
286+
-- Create luatest server objects.
287+
local servers = {}
288+
local server_map = {}
289+
for _, name in ipairs(instance_names) do
290+
local iserver = server:new(fun.chain(server_opts, {
291+
alias = name,
292+
}):tomap())
293+
table.insert(servers, iserver)
294+
server_map[name] = iserver
295+
end
296+
297+
-- Store a cluster object in 'g'.
298+
self._servers = servers
299+
self._server_map = server_map
300+
self._dir = dir
301+
self._config_file_rel = config_file_rel
302+
self._server_opts = server_opts
303+
304+
g._cluster = self
305+
306+
return self
307+
end
308+
309+
-- }}} Replicaset management
310+
311+
-- {{{ Replicaset that can't start
312+
313+
--- Ensure cluster startup error
314+
--
315+
-- Starts a all instance of a cluster from the given config and
316+
-- ensure that all the instances fails to start and reports the
317+
-- given error message.
318+
--
319+
-- @tab config Cluster configuration.
320+
-- @string exp_err Expected error message.
321+
function Cluster:startup_error(config, exp_err)
322+
-- Stub for the linter, since self is unused though
323+
-- we need to be consistent with Cluster:new()
324+
assert(self)
325+
assert(type(config) == 'table')
326+
assert(config._config == nil, "Please provide cbuilder:new():config()")
327+
-- Prepare a temporary directory and write a configuration
328+
-- file.
329+
local dir = treegen.prepare_directory({}, {})
330+
local config_file_rel = 'config.yaml'
331+
local config_file = treegen.write_file(dir, config_file_rel,
332+
yaml.encode(config))
333+
334+
-- Collect names of all the instances defined in the config
335+
-- in the alphabetical order.
336+
local instance_names = instance_names_from_config(config)
337+
338+
for _, name in ipairs(instance_names) do
339+
local env = {}
340+
local args = {'--name', name, '--config', config_file}
341+
local opts = {nojson = true, stderr = true}
342+
local res = justrun.tarantool(dir, env, args, opts)
343+
344+
assertions.assert_equals(res.exit_code, 1)
345+
assertions.assert_str_contains(res.stderr, exp_err)
346+
end
347+
end
348+
349+
-- }}} Replicaset that can't start
350+
351+
hooks.before_all_preloaded(init)
352+
hooks.after_each_preloaded(drop)
353+
hooks.after_all_preloaded(clean)
354+
355+
return Cluster

luatest/init.lua

+5
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,11 @@ luatest.justrun = require('luatest.justrun')
4848
-- @see luatest.cbuilder
4949
luatest.cbuilder = require('luatest.cbuilder')
5050

51+
--- Tarantool cluster management utils.
52+
--
53+
-- @see luatest.cluster
54+
luatest.cluster = require('luatest.cluster')
55+
5156
--- Add before suite hook.
5257
--
5358
-- @function before_suite

0 commit comments

Comments
 (0)