Skip to content

Commit

Permalink
config: add dependencies for roles
Browse files Browse the repository at this point in the history
This patch adds dependencies support for roles.

Part of tarantool#9078

@TarantoolBot document
Title: dependencies for roles

Roles can now have dependencies. This means that the verify() and
apply() methods will be executed for these roles, taking into account
the dependencies. Dependencies should be written in the "dependencies"
field of the array type. Note, the roles will be loaded (not applied!)
in the same order in which they were specified, i.e. not taking
dependencies into account.

Example:

Dependencies of role A: B, C
Dependencies of role B: D
No other role has dependencies.

Order in which roles were given: [E, C, A, B, D, G]
They will be loaded in the same order: [E, C, A, B, D, G]
The order, in which functions verify() and apply() will be executed:
[E, C, D, B, A, G].
  • Loading branch information
ImeevMA committed Nov 28, 2023
1 parent c92500a commit 5815cdf
Show file tree
Hide file tree
Showing 3 changed files with 330 additions and 14 deletions.
3 changes: 3 additions & 0 deletions changelogs/unreleased/gh-9078-role-dependencies.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
## feature/config

* Introduced dependencies for roles (gh-9078).
122 changes: 108 additions & 14 deletions src/box/lua/config/applier/roles.lua
Original file line number Diff line number Diff line change
@@ -1,19 +1,109 @@
local log = require('internal.config.utils.log')

local last_loaded = {}
local last_loaded_names_ordered = {}
local last_roles_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]
local roles_to_stop = {}
for id = #last_roles_ordered, 1, -1 do
local role_name = last_roles_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)
table.insert(roles_to_stop, role_name)
end
end
if #roles_to_stop == 0 then
return
end
local deps = {}
for role_name in pairs(roles_to_skip or {}) do
local role = last_loaded[role_name] or {}
-- There is no need to check transitive dependencies for roles_to_skip,
-- because they were already checked when roles were started, i.e. if
-- role A depends on role B, which depends on role C, and we stop
-- role C, then we will get the error that role B depends on role C.
for _, dep in pairs(role.dependencies or {}) do
deps[dep] = deps[dep] or {}
table.insert(deps[dep], role_name)
end
end
for _, role_name in ipairs(roles_to_stop) do
if deps[role_name] ~= nil then
local err
if #deps[role_name] == 1 then
err =('role %q depends on it'):format(deps[role_name][1])
else
local names = {}
for _, v in ipairs(deps[role_name]) do
table.insert(names, ("%q"):format(v))
end
local names_str = table.concat(names, ', ')
err = ('roles %s depend on it'):format(names_str)
end
error(('Role %q cannot be stopped because %s'):format(role_name,
err), 0)
end
end
for _, role_name in ipairs(roles_to_stop) do
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

local function resort_roles(original_order, roles)
local ordered = {}

-- Needed to detect circular dependencies.
local to_add = {}

-- To skip already added roles.
local added = {}

local function add_role(role_name)
if added[role_name] then
return
end

to_add[role_name] = true

for _, dep in ipairs(roles[role_name].dependencies or {}) do
-- Detect a role that is not in the list of instance's roles.
if not roles[dep] then
local err = 'Role %q requires role %q, but the latter is ' ..
'not in the list of roles of the instance'
error(err:format(role_name, dep), 0)
end

-- Detect a circular dependency.
if to_add[dep] and role_name == dep then
local err = 'Circular dependency: role %q depends on itself'
error(err:format(role_name), 0)
end
if to_add[dep] and role_name ~= dep then
local err = 'Circular dependency: roles %q and %q depend on ' ..
'each other'
error(err:format(role_name, dep), 0)
end

-- Go into the recursion: add the dependency.
add_role(dep)
end

to_add[role_name] = nil
added[role_name] = true
table.insert(ordered, role_name)
end

-- Keep the order, where the dependency tree doesn't obligate
-- us to change it.
for _, role_name in ipairs(original_order) do
assert(roles[role_name] ~= nil)
add_role(role_name)
end

return ordered
end

local function apply(config)
Expand All @@ -37,12 +127,8 @@ local function apply(config)
-- 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.
local loaded = {}
for _, role_name in ipairs(roles_ordered) do
local role = last_loaded[role_name]
if not role then
Expand All @@ -57,10 +143,18 @@ local function apply(config)
end
end
loaded[role_name] = role
table.insert(loaded_names_ordered, role_name)
if role.dependencies ~= nil and type(role.dependencies) ~= 'table' then
local err = 'Role %q has field "dependencies" of type %s, '..
'array-like table or nil expected'
error(err:format(role_name, type(role.dependencies)), 0)
end
end

-- Re-sorting of roles taking into account dependencies between them.
roles_ordered = resort_roles(roles_ordered, loaded)

-- Validate configs for all roles.
local roles_cfg = configdata:get('roles_cfg', {use_default = true}) or {}
for _, role_name in ipairs(roles_ordered) do
local ok, err = pcall(loaded[role_name].validate, roles_cfg[role_name])
if not ok then
Expand All @@ -78,7 +172,7 @@ local function apply(config)
end

last_loaded = loaded
last_loaded_names_ordered = loaded_names_ordered
last_roles_ordered = roles_ordered
end

return {
Expand Down
219 changes: 219 additions & 0 deletions test/config-luatest/roles_test.lua
Original file line number Diff line number Diff line change
Expand Up @@ -353,3 +353,222 @@ g.test_role_reload_error = function(g)
exp_err = 'Error stopping role one: Wrongly stopped'
})
end

-- Make sure dependencies in roles works as intended.
g.test_role_dependencies_success = function(g)
local one = [[
local function apply()
_G.foo = _G.foo .. '_one'
end
return {
validate = function() end,
apply = apply,
stop = function() end,
}
]]

local two = [[
local function apply()
_G.foo = _G.foo .. '_two'
end
return {
dependencies = {'one'},
validate = function() end,
apply = apply,
stop = function() end,
}
]]

local three = [[
local function apply()
_G.foo = _G.foo .. '_three'
end
return {
dependencies = {'four', 'two'},
validate = function() end,
apply = apply,
stop = function() end,
}
]]

local four = [[
_G.foo = 'four'
return {
validate = function() end,
apply = function() end,
stop = function() end,
}
]]

local five = [[
local function apply()
_G.foo = _G.foo .. '_five'
end
return {
validate = function() end,
apply = apply,
stop = function() end,
}
]]

local verify = function()
t.assert_equals(_G.foo, 'four_one_two_three_five')
end

helpers.success_case(g, {
roles = {one = one, two = two, three = three, four = four, five = five},
options = {
['roles'] = {'four', 'three', 'two', 'one', 'five'}
},
verify = verify,
})
end

g.test_role_dependencies_error_wrong_type = function(g)
local one = [[
return {
dependencies = 'two',
validate = function() end,
apply = function() end,
stop = function() end,
}
]]

helpers.failure_case(g, {
roles = {one = one},
options = {
['roles'] = {'one'}
},
exp_err = 'Role "one" has field "dependencies" of type string, '..
'array-like table or nil expected'
})
end

g.test_role_dependencies_error_no_role = function(g)
local one = [[
return {
dependencies = {'two'},
validate = function() end,
apply = function() end,
stop = function() end,
}
]]

helpers.failure_case(g, {
roles = {one = one},
options = {
['roles'] = {'one'}
},
exp_err = 'Role "one" requires role "two", but the latter is not in ' ..
'the list of roles of the instance'
})
end

g.test_role_dependencies_error_self = function(g)
local one = [[
return {
dependencies = {'one'},
validate = function() end,
apply = function() end,
stop = function() end,
}
]]

helpers.failure_case(g, {
roles = {one = one},
options = {
['roles'] = {'one'}
},
exp_err = 'Circular dependency: role "one" depends on itself'
})
end

g.test_role_dependencies_error_circular = function(g)
local one = [[
return {
dependencies = {'two'},
validate = function() end,
apply = function() end,
stop = function() end,
}
]]

local two = [[
return {
dependencies = {'one'},
validate = function() end,
apply = function() end,
stop = function() end,
}
]]

helpers.failure_case(g, {
roles = {one = one, two = two},
options = {
['roles'] = {'one', 'two'}
},
exp_err = 'Circular dependency: roles "two" and "one" depend on ' ..
'each other'
})
end

g.test_role_dependencies_stop_required_role = function(g)
local one = [[
return {
dependencies = {'two', 'three'},
validate = function() end,
apply = function() end,
stop = function() end,
}
]]

local two = [[
return {
dependencies = {'three'},
validate = function() end,
apply = function() end,
stop = function() end,
}
]]

local three = [[
return {
validate = function() end,
apply = function() end,
stop = function() end,
}
]]

helpers.reload_failure_case(g, {
roles = {one = one, two = two, three = three},
roles_2 = {one = one, three = three},
options = {
['roles'] = {'one', 'two', 'three'}
},
options_2 = {
['roles'] = {'one', 'three'}
},
verify = function() end,
exp_err = 'Role "two" cannot be stopped because role "one" ' ..
'depends on it'
})

helpers.reload_failure_case(g, {
roles = {one = one, two = two, three = three},
roles_2 = {one = one, two = two},
options = {
['roles'] = {'one', 'two', 'three'}
},
options_2 = {
['roles'] = {'one', 'two'}
},
verify = function() end,
exp_err = 'Role "three" cannot be stopped because roles ' ..
'"one", "two" depend on it'
})
end

0 comments on commit 5815cdf

Please sign in to comment.