Skip to content

Commit

Permalink
cartridge: ability to use persistent functions
Browse files Browse the repository at this point in the history
User can configure the role with persistent functions as callbacks for
task.

Closes #153
  • Loading branch information
Ivan Bannikov authored and oleg-jukovec committed Aug 22, 2023
1 parent 94c5638 commit 30aaef5
Show file tree
Hide file tree
Showing 6 changed files with 144 additions and 3 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.

### Added

- Added ability to use persistent functions in `box.func` with cartridge. User can configure
the role with persistent functions as callback for task.

### Changed

### Fixed
Expand Down
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,16 @@ package with features:
only affects the current node and does not update values in the clusterwide
configuration. The manual change will be overwritten by a next
`apply_config` call.
* You can use persistent functions (i.e. created by `box.schema.func.create`).
When configuring, role tries firstly get function from global namespace
(`_G`) and if function was not found then role tries search in `box.func` for
function with the same name.
Be careful! At the moment of validating and applying config of expirationd
role all persistent functions must be created before, so to configure
cartridge application correctly you must do it in two steps: at the first
step you have to confgure migrations with creating persistent functions and
run them, at the second one put expirationd config.
* The role stops all expirationd tasks on an instance on the role termination.
* The role can automatically start or kill old tasks from the role
configuration:
Expand Down
29 changes: 27 additions & 2 deletions cartridge/roles/expirationd.lua
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,40 @@ local expirationd = require("expirationd")
local role_name = "expirationd"
local started = require("cartridge.vars").new(role_name)

local function dumb_fn() end

local function load_function(func_name)
if func_name == nil or type(func_name) ~= 'string' then
return nil
end

local func = rawget(_G, func_name)
if func == nil or type(func) ~= 'function' then
if func == nil then
if type(box.cfg) == 'function' then
-- After restart `validate_config` sometimes is run before box.cfg
-- call and it leads to error like `Please call box.cfg first` and
-- at this moment we are not able to check real availability of
-- functions in box.func. If we return nil, we fail configration
-- but it is not an error actually, because we cannot do any
-- checks. Thus we decided to return dumb_fn to avoid
-- misconfiguration at this stage and in hope that `apply_config`
-- will do real check.
-- P.S. Of course it is a bad solution, but...
return dumb_fn
end
if not box.schema.func.exists(func_name) then
return nil
end

return function(...)
return box.func[func_name]:call({...})
end
end

if type(func) ~= 'function' then
return nil
end

return func
end

Expand All @@ -19,7 +44,7 @@ local function get_param(param_name, value, types)
b = {type = "boolean", err = "a boolean"},
n = {type = "number", err = "a number"},
s = {type = "string", err = "a string"},
f = {type = "string", transform = load_function, err = "a function name in _G"},
f = {type = "string", transform = load_function, err = "a function name in _G or in box.func"},
t = {type = "table", err = "a table"},
any = {err = "any type"},
}
Expand Down
7 changes: 7 additions & 0 deletions test/helper.lua
Original file line number Diff line number Diff line change
Expand Up @@ -297,4 +297,11 @@ function helpers.get_error_function(error_msg)
end
end

function helpers.create_persistent_function(name, body)
box.schema.func.create(name, {
body = body or "function(...) return true end",
if_not_exists = true
})
end

return helpers
45 changes: 45 additions & 0 deletions test/integration/role_test.lua
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,51 @@ function g.test_start_task_from_config(cg)
end)
end

-- init/stop/validate_config/apply_config well tested in test/unit/role_test.lua
-- here we just ensure that it works as expected
function g.test_start_task_from_config_with_functions_from_box_func(cg)
t.skip_if(_TARANTOOL < '2', 'Restricted support in Tarantool 1.10')
t.assert_equals(3, cg.cluster:server('s1-master').net_box:eval([[
box.schema.func.create('forever_true_test', {
body = "function(...) return true end",
if_not_exists = true
})
box.space.customers:insert({1})
box.space.customers:insert({2})
box.space.customers:insert({3})
return #box.space.customers:select({}, {limit = 10})
]]))
cg.cluster.main_server:upload_config({
expirationd = {
test_task = {
space = "customers",
is_expired = "forever_true_test",
is_master_only = true,
}
},
})
t.assert_equals(cg.cluster:server('s1-master').net_box:eval([[
local cartridge = require("cartridge")
return #cartridge.service_get("expirationd").tasks()
]]), 1)
helpers.retrying({}, function()
t.assert_equals(cg.cluster:server('s1-master').net_box:eval([[
return #box.space.customers:select({}, {limit = 10})
]]), 0)
end)

-- is_master == false
t.assert_equals(cg.cluster:server("s1-slave").net_box:eval([[
local cartridge = require("cartridge")
return #cartridge.service_get("expirationd").tasks()
]]), 0)
helpers.retrying({}, function()
t.assert_equals(cg.cluster:server('s1-slave').net_box:eval([[
return #box.space.customers:select({}, {limit = 10})
]]), 0)
end)
end

function g.test_continue_after_hotreload(cg)
t.assert_equals(10, cg.cluster:server('s1-master').net_box:eval([[
for i = 1,10 do
Expand Down
53 changes: 52 additions & 1 deletion test/unit/role_test.lua
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ local g = t.group('expirationd_role')
local is_cartridge_roles, _ = pcall(require, 'cartridge.roles')

local always_true_func_name = "expirationd_test_always_true"
local always_true_func_name_in_box_func = 'expirationd_test_always_true_bf'
local always_true_func_name_with_side_effect = 'expirationd_test_always_true_se'
local always_true_func_name_with_side_effect_flag = 'expirationd_test_always_true_se_called'
local iterate_pairs_func_name = "expirationd_test_pairs"

g.before_all(function()
Expand All @@ -25,8 +28,21 @@ g.before_each(function()

g.is_metrics_supported = helpers.is_metrics_supported()

if _TARANTOOL >= '2' then
helpers.create_persistent_function(always_true_func_name_in_box_func)
helpers.create_persistent_function(always_true_func_name_with_side_effect, [[
function(...)
return false
end
]])
end

rawset(_G, always_true_func_name_with_side_effect_flag, false)
rawset(_G, always_true_func_name, function() return true end)
rawset(_G, iterate_pairs_func_name, function() return pairs({}) end)
rawset(_G, always_true_func_name_with_side_effect, function()
rawset(_G, always_true_func_name_with_side_effect_flag, true)
end)
end)

g.after_each(function(g)
Expand Down Expand Up @@ -457,7 +473,7 @@ local options_cases = {
},
start_key_invalid = {
ok = false,
err = "expirationd: options.start_key must be a function name in _G or a table",
err = "expirationd: options.start_key must be a function name in _G or in box.func or a table",
options = {start_key = "any string"},
},
tuples_per_iteration_number = {
Expand Down Expand Up @@ -607,6 +623,41 @@ function g.test_apply_config_start_cfg_task(cg)
t.assert_not_equals(cg.role.task(task_name), nil)
end

function g.test_apply_config_start_cfg_task_with_box_func(cg)
t.skip_if(_TARANTOOL < '2', 'Restricted support in Tarantool 1.10')
local task_name = 'cfg'
cg.role.apply_config({
expirationd = {
[task_name] = {
space = g.space.id,
is_expired = always_true_func_name_in_box_func
}
}
}, { is_master = false })

t.assert_equals(#cg.role.tasks(), 1)
t.assert_not_equals(cg.role.task(task_name), nil)
end

function g.test_apply_config_start_cfg_task_with_correct_order(cg)
t.skip_if(_TARANTOOL < '2', 'Restricted support in Tarantool 1.10')
t.assert_not(rawget(_G, always_true_func_name_with_side_effect_flag))
local task_name = 'cfg'
cg.space:insert({1, "1"})
cg.role.apply_config({
expirationd = {
[task_name] = {
space = g.space.id,
is_expired = always_true_func_name_with_side_effect
}
}
}, { is_master = false })

t.assert_equals(#cg.role.tasks(), 1)
t.assert_not_equals(cg.role.task(task_name), nil)
t.assert(rawget(_G, always_true_func_name_with_side_effect_flag))
end

function g.test_apply_config_start_task_with_all_options(cg)
local task_name = "apply_config_test"
local options = {
Expand Down

0 comments on commit 30aaef5

Please sign in to comment.