Skip to content

Commit

Permalink
config: introduce roles
Browse files Browse the repository at this point in the history
This patch introduces initial support for roles.

Roles are programs that run when a configuration is loaded or reloaded.
Roles can be defined for each instance. On first run, the role is
initialized with the init() method. On each run, the role calls
validate_config() and then apply_config() if validate_config() returns
true. If the roles were removed from the instance on reboot, the role is
stopped using the stop() method.

If a role was added to an instance, then removed from the instance, and
then added again, init() is still not called.

If a role is defined more than once on an instance, it will still only
run once.

Roles must have four methods: init(), validate_config(), apply_config()
and stop().

Each assigned role must be described in the roles_cfg file.

Dependencies are not currently supported for roles.

Part of tarantool#9078

NO_DOC=will be added later.
  • Loading branch information
ImeevMA committed Sep 3, 2023
1 parent f58cc96 commit 1a61576
Show file tree
Hide file tree
Showing 10 changed files with 645 additions and 37 deletions.
3 changes: 3 additions & 0 deletions changelogs/unreleased/gh-9078-roles.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
## feature/config

* Added initial support of roles (gh-9078).
1 change: 1 addition & 0 deletions src/box/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ lua_source(lua_sources lua/config/applier/console.lua config_applier_console
lua_source(lua_sources lua/config/applier/credentials.lua config_applier_credentials_lua)
lua_source(lua_sources lua/config/applier/fiber.lua config_applier_fiber_lua)
lua_source(lua_sources lua/config/applier/mkdir.lua config_applier_mkdir_lua)
lua_source(lua_sources lua/config/applier/roles.lua config_applier_roles_lua)
lua_source(lua_sources lua/config/applier/sharding.lua config_applier_sharding_lua)
lua_source(lua_sources lua/config/cluster_config.lua config_cluster_config_lua)
lua_source(lua_sources lua/config/configdata.lua config_configdata_lua)
Expand Down
75 changes: 75 additions & 0 deletions src/box/lua/config/applier/roles.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
local log = require('internal.config.utils.log')

local last_loaded = {}

local function apply(_config)
log.verbose('roles.apply: do nothing')
end

local function post_apply(config)
local configdata = config._configdata
local role_names = configdata:get('roles', {use_default = true})
if role_names == nil or next(role_names) == nil then
return
end

-- Remove duplicates.
local roles = {}
local roles_dup = {}
local roles_check = {}
for _, role_name in pairs(role_names) do
table.insert(roles_dup, roles_check[role_name] ~= nil)
roles_check[role_name] = true
end
for id, role_name in pairs(role_names) do
if not roles_dup[id] then
table.insert(roles, role_name)
end
end

-- Run roles.
local roles_cfg = configdata:get('roles_cfg', {use_default = true}) or {}
local loaded = {}
for _, role_name in pairs(roles) do
local role_cfg = roles_cfg[role_name]
-- All roles must be described in roles_cfg, even if the role does not
-- require a cfg.
if role_cfg == nil then
error('No config for role ' .. role_name, 0)
end
local role = last_loaded[role_name]
if not role then
role = require(role_name)
local methods = {'init', 'validate_config', 'apply_config', 'stop'}
for _, method_name in pairs(methods) do
if type(role[method_name]) ~= 'function' then
local err = 'Role %s does not contain method %s'
error(err:format(role_name, method_name), 0)
end
end
log.verbose('roles.post_apply: initialize role ' .. role_name)
role.init()
end
loaded[role_name] = role
if not role.validate_config(role_cfg) then
error('Wrong config for role ' .. role_name, 0)
end
log.verbose('roles.post_apply: apply config for role ' .. role_name)
role.apply_config(role_cfg)
end

-- Stop removed roles.
for role_name, role in pairs(last_loaded) do
if not roles_check[role_name] then
log.verbose('roles.post_apply: stop role ' .. role_name)
role.stop()
end
end
last_loaded = loaded
end

return {
name = 'roles',
apply = apply,
post_apply = post_apply,
}
1 change: 1 addition & 0 deletions src/box/lua/config/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ function methods._initialize(self)
self:_register_applier(require('internal.config.applier.console'))
self:_register_applier(require('internal.config.applier.fiber'))
self:_register_applier(require('internal.config.applier.sharding'))
self:_register_applier(require('internal.config.applier.roles'))
self:_register_applier(require('internal.config.applier.app'))

if extras ~= nil then
Expand Down
7 changes: 7 additions & 0 deletions src/box/lua/config/instance_config.lua
Original file line number Diff line number Diff line change
Expand Up @@ -1802,6 +1802,13 @@ return schema.new('instance_config', schema.record({
"compatibility",
}),
})),
roles_cfg = schema.map({
key = schema.scalar({type = 'string'}),
value = schema.scalar({type = 'any'}),
}),
roles = schema.array({
items = schema.scalar({type = 'string'})
}),
}, {
-- This kind of validation cannot be implemented as the
-- 'validate' annotation of a particular schema node. There
Expand Down
5 changes: 5 additions & 0 deletions src/box/lua/init.c
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ extern char session_lua[],
config_applier_credentials_lua[],
config_applier_fiber_lua[],
config_applier_mkdir_lua[],
config_applier_roles_lua[],
config_applier_sharding_lua[],
config_cluster_config_lua[],
config_configdata_lua[],
Expand Down Expand Up @@ -387,6 +388,10 @@ static const char *lua_sources[] = {
"internal.config.applier.sharding",
config_applier_sharding_lua,

"config/applier/roles",
"internal.config.applier.roles",
config_applier_roles_lua,

"config/init",
"config",
config_init_lua,
Expand Down
2 changes: 2 additions & 0 deletions test/config-luatest/app_test.lua
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ g.test_reload_failure = function(g)
script = '',
options = {['app.file'] = 'main.lua'},
verify = function() end,
options_2 = {['app.file'] = 'main.lua'},
script_2 = ([[
error('%s', 0)
]]):format(err_msg),
Expand All @@ -101,6 +102,7 @@ g.test_reload_success = function(g)
script_2 = ([[
error('%s', 0)
]]):format(err_msg),
options_2 = {['app.module'] = 'main'},
verify_2 = function()
local config = require('config')
local info = config:info()
Expand Down
45 changes: 40 additions & 5 deletions test/config-luatest/helpers.lua
Original file line number Diff line number Diff line change
Expand Up @@ -88,13 +88,20 @@ local simple_config = {

local function prepare_case(g, opts)
local dir = opts.dir
local roles = opts.roles
local script = opts.script
local options = opts.options

if dir == nil then
dir = treegen.prepare_directory(g, {}, {})
end

if roles ~= nil and next(roles) ~= nil then
for name, body in pairs(roles) do
treegen.write_script(dir, name .. '.lua', body)
end
end

if script ~= nil then
treegen.write_script(dir, 'main.lua', script)
end
Expand Down Expand Up @@ -135,6 +142,10 @@ end
-- Start a server with the given script and the given
-- configuration, run a verification function on it.
--
-- * opts.roles
--
-- Role codes for writing into corresponding files.
--
-- * opts.script
--
-- Code write into the main.lua file.
Expand Down Expand Up @@ -164,6 +175,7 @@ end
-- Start tarantool process with the given script/config and check
-- the error.
--
-- * opts.roles
-- * opts.script
-- * opts.options
--
Expand All @@ -185,31 +197,43 @@ end
-- Start a server, write a new script/config, reload, run a
-- verification function.
--
-- * opts.roles
-- * opts.script
-- * opts.options
-- * opts.verify
--
-- Same as in success_case().
--
-- * opts.roles_2
--
-- A new list of roles to prepare before config:reload().
--
-- * opts.script_2
--
-- A new script to write into the main.lua file before
-- config:reload().
--
-- * opts.options_2
--
-- A new options for config:reload().
--
-- * opts.verify_2
--
-- Verify test invariants after config:reload().
local function reload_success_case(g, opts)
assert(opts.options)
local roles_2 = opts.roles_2
local script_2 = opts.script_2
local options = assert(opts.options)
local options_2 = opts.options_2
local verify_2 = assert(opts.verify_2)

local prepared = success_case(g, opts)

prepare_case(g, {
dir = prepared.dir,
roles = roles_2,
script = script_2,
options = options,
options = options_2,
})
g.server:exec(function()
local config = require('config')
Expand All @@ -221,31 +245,42 @@ end
-- Start a server, write a new script/config, reload, run a
-- verification function.
--
-- * opts.roles
-- * opts.script
-- * opts.options
-- * opts.verify
--
-- Same as in success_case().
--
-- * opts.roles_2
--
-- A new list of roles to prepare before config:reload().
--
-- * opts.script_2
--
-- A new script to write into the main.lua file before
-- config:reload().
--
-- * opts.options_2
--
-- A new options for config:reload().
--
-- * opts.exp_err
--
-- An error that config:reload() must raise.
local function reload_failure_case(g, opts)
local script_2 = assert(opts.script_2)
local options = assert(opts.options)
local roles_2 = opts.roles_2
local script_2 = opts.script_2
local options_2 = opts.options_2
local exp_err = assert(opts.exp_err)

local prepared = success_case(g, opts)

prepare_case(g, {
dir = prepared.dir,
roles = roles_2,
script = script_2,
options = options,
options = options_2,
})
t.assert_error_msg_equals(exp_err, g.server.exec, g.server, function()
local config = require('config')
Expand Down
71 changes: 39 additions & 32 deletions test/config-luatest/mkdir_test.lua
Original file line number Diff line number Diff line change
Expand Up @@ -49,24 +49,28 @@ g.test_work_dir = function(g)
]]):format(dir))

local data_dir = fio.pathjoin(dir, '{{ instance_name }}')

local options = {
['process.work_dir'] = 'a/b/c',

-- Place all the necessary directories and files into
-- a location outside of the work_dir.
--
-- It allows to test the work_dir creation logic
-- independently of creation of other directories.
['process.pid_file'] = fio.pathjoin(dir, '{{ instance_name }}.pid'),
['vinyl.dir'] = data_dir,
['wal.dir'] = data_dir,
['snapshot.dir'] = data_dir,
['console.socket'] = fio.pathjoin(dir,
'{{ instance_name }}.control'),
['iproto.listen'] = fio.pathjoin(dir, '{{ instance_name }}.iproto'),
}

helpers.reload_success_case(g, {
dir = dir,
options = {
['process.work_dir'] = 'a/b/c',

-- Place all the necessary directories and files into
-- a location outside of the work_dir.
--
-- It allows to test the work_dir creation logic
-- independently of creation of other directories.
['process.pid_file'] = fio.pathjoin(dir, '{{ instance_name }}.pid'),
['vinyl.dir'] = data_dir,
['wal.dir'] = data_dir,
['snapshot.dir'] = data_dir,
['console.socket'] = fio.pathjoin(dir,
'{{ instance_name }}.control'),
['iproto.listen'] = fio.pathjoin(dir, '{{ instance_name }}.iproto'),
},
options = options,
options_2 = options,
verify = verify,
verify_2 = verify,
})
Expand Down Expand Up @@ -106,24 +110,27 @@ g.test_dirs_are_relative_to_work_dir = function(g)
assert(not fio.path.exists(fio.pathjoin(work_dir, 'a')))
]]):format(dir))

local options = {
['process.work_dir'] = 'a/b/c',
['process.pid_file'] = 'd/{{ instance_name }}.pid',
['vinyl.dir'] = 'e',
['wal.dir'] = 'f',
['snapshot.dir'] = 'g',
['console.socket'] = 'h/{{ instance_name }}.control',
['log.to'] = 'file',
['log.file'] = 'i/{{ instance_name }}.log',

-- Set the binary protocol socket as an absolute path
-- to don't confuse the testing helpers and allows
-- them to connect to the instance and execute
-- necessary commands (such as `config:reload()`).
['iproto.listen'] = fio.pathjoin(dir, '{{ instance_name }}.iproto'),
}

helpers.reload_success_case(g, {
dir = dir,
options = {
['process.work_dir'] = 'a/b/c',
['process.pid_file'] = 'd/{{ instance_name }}.pid',
['vinyl.dir'] = 'e',
['wal.dir'] = 'f',
['snapshot.dir'] = 'g',
['console.socket'] = 'h/{{ instance_name }}.control',
['log.to'] = 'file',
['log.file'] = 'i/{{ instance_name }}.log',

-- Set the binary protocol socket as an absolute path
-- to don't confuse the testing helpers and allows
-- them to connect to the instance and execute
-- necessary commands (such as `config:reload()`).
['iproto.listen'] = fio.pathjoin(dir, '{{ instance_name }}.iproto'),
},
options = options,
options_2 = options,
verify = verify,
verify_2 = verify,
})
Expand Down

0 comments on commit 1a61576

Please sign in to comment.