Skip to content

Commit 902f6db

Browse files
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 902f6db

File tree

5 files changed

+598
-1
lines changed

5 files changed

+598
-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

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

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)