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 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 7, 2023
1 parent 4f85d27 commit 79287dc
Show file tree
Hide file tree
Showing 3 changed files with 233 additions and 10 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

* Dependencies for roles is introduced (gh-9078).
79 changes: 69 additions & 10 deletions src/box/lua/config/applier/roles.lua
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
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]
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)
Expand All @@ -16,6 +16,60 @@ local function stop_roles(roles_to_skip)
end
end

local function resort_roles(original_order, roles_dependencies)
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_dependencies[role_name].dependencies) do
-- Detect a role that is not in the list of instance's roles.
if not roles_dependencies[dep] then
local err = 'Role %s requires role %s, 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 %s depends on itself'
error(err:format(role_name), 0)
end
if to_add[dep] and role_name ~= dep then
local err = 'Circular dependency: roles %s and %s 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_dependencies[role_name] ~= nil)
add_role(role_name)
end

return ordered
end

local function apply(config)
local configdata = config._configdata
local role_names = configdata:get('roles', {use_default = true})
Expand All @@ -37,12 +91,9 @@ 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 = {}
local roles_dependencies = {}
for _, role_name in ipairs(roles_ordered) do
local role = last_loaded[role_name]
if not role then
Expand All @@ -57,10 +108,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 = 'Wrong type of role %s dependencies'
error(err:format(role_name), 0)
end
roles_dependencies[role_name] = {dependencies = role.dependencies or {}}
end

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

-- 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 +137,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
161 changes: 161 additions & 0 deletions test/config-luatest/roles_test.lua
Original file line number Diff line number Diff line change
Expand Up @@ -353,3 +353,164 @@ 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 = 'Wrong type of role one dependencies'
})
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

0 comments on commit 79287dc

Please sign in to comment.