Skip to content

Commit 8dfbae0

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 8dfbae0

File tree

5 files changed

+569
-1
lines changed

5 files changed

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

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

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)