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. Dependencies are not
currently supported for roles.

Part of tarantool#9078

@TarantoolBot document
Title: Roles

Two new options have been added: "roles" and "roles_cfg". The first one
is an array and the second one is a map. Each of these can be defined
per instance, replica set, group, and globally. As with almost all other
options, with the exception of those defined as 'map', the 'roles'
option for the lower scope will replace the roles for the higher scope.
Value roles_cfg however defined as "map", so it will be merged.

The "roles" option defines the roles for each instance. A role is a
program that runs when a configuration is loaded or reloaded. If a role
is defined more than once on an instance, it will still only be run
once. Three functions must be defined in the role: validate(), apply()
and stop(). Each of these functions should throw an error if it occurs.

The "roles_cfg" option specifies the configuration for each role. In
this option, the role name is the key and the role configuration is the
value.

On each run, all roles will be loaded (if necessary) in the order in
which they were specified; the configuration for each role will then be
validated using the corresponding validate() function in the same order;
and then they will all be run with apply() function in the same order.
If some roles have been removed from the instance, they will be stopped
in reverse order using the stop() function.

Example of a role structure:
```
local M = {}

-- Validates configuration of the role.
--
-- Called on initial configuration apply at startup and on
-- configuration reload if the role is enabled for the given instance.
--
-- The cfg argument may have arbitrary user provided value,
-- including nil.
--
-- Must raise an error if the validation fail.
function M.validate(cfg)
    -- <...>
end

-- Applies the given configuration of the role.
--
-- Called on initial configuration apply at startup and on
-- configuration reload if the role is enabled for the given instance.
--
-- The cfg argument may have arbitrary user provided value,
-- including nil.
--
-- Must raise an error if the given configuration can't be applied.
function M.apply(cfg)
    -- <...>
end

-- Stops the role.
--
-- Called on configuration reload if the role was enabled before
-- and removed now from the list of roles of the given instance.
--
-- Should cancel all background fibers and clean up hold
-- resources.
--
-- Must raise an error if this action can't be performed.
function M.stop()
    -- <...>
end

return M
```
  • Loading branch information
ImeevMA committed Sep 8, 2023
1 parent 0dc3735 commit 81f4d6d
Show file tree
Hide file tree
Showing 8 changed files with 494 additions and 2 deletions.
4 changes: 4 additions & 0 deletions changelogs/unreleased/gh-9078-roles.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
## feature/config

* Introduced the initial support for roles - programs that run when
a configuration is loaded or reloaded (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
87 changes: 87 additions & 0 deletions src/box/lua/config/applier/roles.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
local log = require('internal.config.utils.log')

local last_loaded = {}
local last_loaded_names_ordered = {}

local function stop_roles(roles_to_skip)
for id = #last_loaded_names_ordered, 1, -1 do
local role_name = last_loaded_names_ordered[id]
if roles_to_skip == nil or roles_to_skip[role_name] == nil then
log.verbose('roles.apply: stop role ' .. role_name)
local ok, err = pcall(last_loaded[role_name].stop)
if not ok then
error(('Error stopping role %s: %s'):format(role_name, err), 0)
end
end
end
end

local function 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
stop_roles()
return
end

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

-- Stop removed roles.
stop_roles(roles)

-- Run roles.
local roles_cfg = configdata:get('roles_cfg', {use_default = true}) or {}
local loaded = {}
local loaded_names_ordered = {}

-- Load roles.
for _, role_name in ipairs(roles_ordered) do
local role = last_loaded[role_name]
if not role then
log.verbose('roles.apply: load role ' .. role_name)
role = require(role_name)
local funcs = {'validate', 'apply', 'stop'}
for _, func_name in pairs(funcs) do
if type(role[func_name]) ~= 'function' then
local err = 'Role %s does not contain function %s'
error(err:format(role_name, func_name), 0)
end
end
end
loaded[role_name] = role
table.insert(loaded_names_ordered, role_name)
end

-- Validate configs for all roles.
for _, role_name in ipairs(roles_ordered) do
local ok, err = pcall(loaded[role_name].validate, roles_cfg[role_name])
if not ok then
error(('Wrong config for role %s: %s'):format(role_name, err), 0)
end
end

-- Apply configs for all roles.
for _, role_name in ipairs(roles_ordered) do
log.verbose('roles.apply: apply config for role ' .. role_name)
local ok, err = pcall(loaded[role_name].apply, roles_cfg[role_name])
if not ok then
error(('Error applying role %s: %s'):format(role_name, err), 0)
end
end

last_loaded = loaded
last_loaded_names_ordered = loaded_names_ordered
end

return {
name = 'roles',
apply = 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
36 changes: 34 additions & 2 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,12 +197,17 @@ 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
Expand All @@ -205,6 +222,7 @@ end
--
-- Verify test invariants after config:reload().
local function reload_success_case(g, opts)
local roles_2 = opts.roles_2
local script_2 = opts.script_2
local options = assert(opts.options)
local verify_2 = assert(opts.verify_2)
Expand All @@ -214,6 +232,7 @@ local function reload_success_case(g, opts)

prepare_case(g, {
dir = prepared.dir,
roles = roles_2,
script = script_2,
options = options_2,
})
Expand All @@ -227,31 +246,44 @@ 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 config to use for the config:reload(). It is optional,
-- if not provided opts.options is used instead.
--
-- * 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 script_2 = opts.script_2
local roles_2 = opts.roles_2
local options = assert(opts.options)
local options_2 = opts.options_2 or options
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

0 comments on commit 81f4d6d

Please sign in to comment.