Skip to content

Commit 5431a1c

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 5431a1c

File tree

5 files changed

+570
-1
lines changed

5 files changed

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

+337
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,337 @@
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(server)
83+
f(server)
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(server)
100+
server: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(server)
111+
server: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(server)
123+
helpers.retrying({timeout = 60}, function()
124+
assertions.assert_equals(server: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 server = self._server_map[instance_name]
137+
assert(server ~= nil)
138+
server:start()
139+
end
140+
141+
--- Stop the whole cluster.
142+
local function cluster_stop(self)
143+
for _, server in ipairs(self._servers or {}) do
144+
server:stop()
145+
end
146+
end
147+
148+
--- Drop the cluster's servers.
149+
local function cluster_drop(self)
150+
for _, server in ipairs(self._servers or {}) do
151+
server: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 server = server:new(fun.chain(self._server_opts, {
175+
alias = name,
176+
}):tomap())
177+
table.insert(self._servers, i, server)
178+
self._server_map[name] = server
179+
end
180+
end
181+
end
182+
183+
--- Reload configuration on all the instances.
184+
--
185+
-- @tab[opt] config New config.
186+
local function cluster_reload(self, config)
187+
assert(config == nil or type(config) == 'table')
188+
189+
-- Rewrite the configuration file if a new config is provided.
190+
if config ~= nil then
191+
treegen.write_file(self._dir, self._config_file_rel,
192+
yaml.encode(config))
193+
end
194+
195+
-- Reload config on all the instances.
196+
self:each(function(server)
197+
-- Assume that all the instances are started.
198+
--
199+
-- This requirement may be relaxed if needed, it is just
200+
-- for simplicity.
201+
assert(server.process ~= nil)
202+
203+
server:exec(function()
204+
local config = require('config')
205+
206+
config:reload()
207+
end)
208+
end)
209+
end
210+
211+
local methods = {
212+
each = cluster_each,
213+
size = cluster_size,
214+
start = cluster_start,
215+
start_instance = cluster_start_instance,
216+
stop = cluster_stop,
217+
drop = cluster_drop,
218+
sync = cluster_sync,
219+
reload = cluster_reload,
220+
}
221+
222+
local cluster_mt = {
223+
__index = function(self, k)
224+
if methods[k] ~= nil then
225+
return methods[k]
226+
end
227+
if self._server_map[k] ~= nil then
228+
return self._server_map[k]
229+
end
230+
return rawget(self, k)
231+
end
232+
}
233+
234+
--- Create a new Tarantool cluster.
235+
--
236+
-- @tab config Cluster configuration.
237+
-- @tab[opt] server_opts Extra options passed to server:new().
238+
-- @tab[opt] opts Cluster options.
239+
-- @string[opt] opts.dir Specific directory for the cluster.
240+
-- @return table
241+
local function new(g, config, server_opts, opts)
242+
assert(type(config) == 'table')
243+
assert(config._config == nil, "Please provide cbuilder:new():config()")
244+
assert(g._cluster == nil)
245+
246+
-- Prepare a temporary directory and write a configuration
247+
-- file.
248+
local dir = opts and opts.dir or treegen.prepare_directory({}, {})
249+
local config_file_rel = 'config.yaml'
250+
local config_file = treegen.write_file(dir, config_file_rel,
251+
yaml.encode(config))
252+
253+
-- Collect names of all the instances defined in the config
254+
-- in the alphabetical order.
255+
local instance_names = instance_names_from_config(config)
256+
257+
assert(next(instance_names) ~= nil, 'No instances in the supplied config')
258+
259+
-- Generate luatest server options.
260+
local server_opts = fun.chain({
261+
config_file = config_file,
262+
chdir = dir,
263+
net_box_credentials = {
264+
user = 'client',
265+
password = 'secret',
266+
},
267+
}, server_opts or {}):tomap()
268+
269+
-- Create luatest server objects.
270+
local servers = {}
271+
local server_map = {}
272+
for _, name in ipairs(instance_names) do
273+
local server = server:new(fun.chain(server_opts, {
274+
alias = name,
275+
}):tomap())
276+
table.insert(servers, server)
277+
server_map[name] = server
278+
end
279+
280+
-- Create a cluster object and store it in 'g'.
281+
g._cluster = setmetatable({
282+
_servers = servers,
283+
_server_map = server_map,
284+
_dir = dir,
285+
_config_file_rel = config_file_rel,
286+
_server_opts = server_opts,
287+
}, cluster_mt)
288+
return g._cluster
289+
end
290+
291+
-- }}} Replicaset management
292+
293+
-- {{{ Replicaset that can't start
294+
295+
--- Ensure cluster startup error
296+
--
297+
-- Starts a all instance of a cluster from the given config and
298+
-- ensure that all the instances fails to start and reports the
299+
-- given error message.
300+
--
301+
-- @tab config Cluster configuration.
302+
-- @string exp_err Expected error message.
303+
local function startup_error(g, config, exp_err)
304+
assert(g) -- temporary stub to not fail luacheck due to unused var
305+
assert(type(config) == 'table')
306+
assert(config._config == nil, "Please provide cbuilder:new():config()")
307+
-- Prepare a temporary directory and write a configuration
308+
-- file.
309+
local dir = treegen.prepare_directory({}, {})
310+
local config_file_rel = 'config.yaml'
311+
local config_file = treegen.write_file(dir, config_file_rel,
312+
yaml.encode(config))
313+
314+
-- Collect names of all the instances defined in the config
315+
-- in the alphabetical order.
316+
local instance_names = instance_names_from_config(config)
317+
318+
for _, name in ipairs(instance_names) do
319+
local env = {}
320+
local args = {'--name', name, '--config', config_file}
321+
local opts = {nojson = true, stderr = true}
322+
local res = justrun.tarantool(dir, env, args, opts)
323+
324+
assertions.assert_equals(res.exit_code, 1)
325+
assertions.assert_str_contains(res.stderr, exp_err)
326+
end
327+
end
328+
329+
-- }}} Replicaset that can't start
330+
331+
hooks.after_each_preloaded(drop)
332+
hooks.after_all_preloaded(clean)
333+
334+
return {
335+
new = new,
336+
startup_error = startup_error,
337+
}

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)