Skip to content
Snippets Groups Projects
Commit 859df3a7 authored by Maria's avatar Maria Committed by Kirill Yukhin
Browse files

box: check whether box is loaded in box.execute()


box.execute() initializes box if it is not initialized. For this sake,
box.execute() is another function (so called <box_load_and_execute>)
when box is not loaded: it loads box and calls 'real' box.execute().
However it is not enough: <box_load_and_execute> may be saved by a user
before box initialization and called after box loading or during box
loading from a separate fiber.

Note: calling <box_load_and_execute> during box loading is safe now, but
calling of box.execute() is not: the 'real' box.execute() does not
verify whether box is configured. It will be fixed in a further commit.

This commit changes <box_load_and_execute> to verify whether box is
initialized and to load box only when it is not loaded. Also it adds
appropriate locking around load_cfg() invocation from
<box_load_and_execute>.

While we're here, clarified contracts of functions that set box
configuration options.

Closes #4231

Co-developed-by: default avatarAlexander Turenko <alexander.turenko@tarantool.org>
parent ebfd2d1d
No related branches found
No related tags found
No related merge requests found
......@@ -241,6 +241,11 @@ local function check_replicaset_uuid()
end
-- dynamically settable options
--
-- Note: An option should be in <dynamic_cfg_skip_at_load> table
-- or should be verified in box_check_config(). Otherwise
-- load_cfg() may report an error, but box will be configured in
-- fact.
local dynamic_cfg = {
listen = private.cfg_set_listen,
replication = private.cfg_set_replication,
......@@ -582,6 +587,16 @@ setmetatable(box, {
end
})
-- Whether box is loaded.
--
-- `false` when box is not configured or when the initialization
-- is in progress.
--
-- `true` when box is configured.
--
-- Use locked() wrapper to obtain reliable results.
local box_is_configured = false
local function load_cfg(cfg)
cfg = upgrade_cfg(cfg, translate_cfg)
cfg = prepare_cfg(cfg, default_cfg, template_cfg,
......@@ -593,6 +608,16 @@ local function load_cfg(cfg)
box.cfg = locked(load_cfg) -- restore original box.cfg
return box.error() -- re-throw exception from check_cfg()
end
-- NB: After this point the function should not raise an
-- error.
--
-- This is important to have right <box_is_configured> (this
-- file) and <is_box_configured> (box.cc) values.
--
-- It also would be counter-intuitive to receive an error from
-- box.cfg({<...>}), but find that box is actually configured.
-- Restore box members after initial configuration
for k, v in pairs(box_configured) do
box[k] = v
......@@ -606,7 +631,13 @@ local function load_cfg(cfg)
end,
__call = locked(reload_cfg),
})
-- This call either succeeds or calls panic() / exit().
private.cfg_load()
-- This block does not raise an error: all necessary checks
-- already performed in private.cfg_check(). See <dynamic_cfg>
-- comment.
for key, fun in pairs(dynamic_cfg) do
local val = cfg[key]
if val ~= nil then
......@@ -621,22 +652,52 @@ local function load_cfg(cfg)
end
end
end
if not box.cfg.read_only and not box.cfg.replication and
not box.error.injection.get('ERRINJ_AUTO_UPGRADE') then
box.schema.upgrade{auto = true}
end
box_is_configured = true
end
box.cfg = locked(load_cfg)
--
-- This makes possible do box.execute without calling box.cfg
-- manually. The load_cfg call overwrites following table and
-- metatable.
-- manually. The load_cfg() call overwrites box.execute, see
-- <box_configured> variable.
--
-- box.execute is <box_load_and_execute> when box is not loaded,
-- <lbox_execute> otherwise. <box_load_and_execute> loads box and
-- calls <lbox_execute>.
--
function box.execute(...)
load_cfg()
local box_load_and_execute
box_load_and_execute = function(...)
-- Configure box if it is not configured, no-op otherwise (not
-- a reconfiguration).
--
-- We should check whether box is configured, because a user
-- may save <box_load_and_execute> function before box loading
-- and call it afterwards.
if not box_is_configured then
locked(function()
-- Re-check, because after releasing of the lock the
-- box state may be changed. We should call load_cfg()
-- only once.
if not box_is_configured then
load_cfg()
end
end)()
end
-- At this point box should be configured and box.execute()
-- function should be replaced with lbox_execute().
assert(type(box.cfg) ~= 'function')
assert(box.execute ~= box_load_and_execute)
return box.execute(...)
end
box.execute = box_load_and_execute
-- gh-810:
-- hack luajit default cpath
......
#!/usr/bin/env tarantool
--
-- gh-4231: box.execute should be an idempotent function meaning
-- its effect should be the same if the user chooses to save it
-- before explicit box.cfg{} invocation and use the saved version
-- afterwards.
--
local tap = require('tap')
local test = tap.test('gh-4231-box-execute-idempotence')
test:plan(3)
local box_load_and_execute = box.execute
-- box is not initialized after invalid box.cfg() call and
-- box.execute() should initialize it first.
pcall(box.cfg, {listen = 'invalid'})
local ok, res = pcall(box.execute, 'SELECT 1')
test:ok(ok, 'verify box.execute() after invalid box.cfg() call', {err = res})
-- Make test cases independent: if the box.execute() call above
-- fails, initialize box explicitly for the next test cases.
box.cfg{}
-- This call should be successful and should skip box
-- (re)initialization.
local ok, res = pcall(box_load_and_execute, 'SELECT 1')
test:ok(ok, 'verify box_load_and_execute after successful box.cfg() call',
{err = res})
-- Just in case: verify usual box.execute() after
-- box_load_and_execute().
local ok, res = pcall(box.execute, 'SELECT 1')
test:ok(ok, 'verify box.execute() after box_load_and_execute()', {err = res})
os.exit(test:check() and 0 or 1)
#!/usr/bin/env tarantool
-- gh-4231: run box_load_and_execute() (it is box.execute value
-- before box will be loaded) in several fibers in parallel and
-- ensure that it returns correct results (i.e. that the function
-- waits until box will be fully configured).
local fiber = require('fiber')
local tap = require('tap')
local box_load_and_execute = box.execute
local fiber_count = 10
local results = fiber.channel(fiber_count)
local function select_from_vindex()
local res = box_load_and_execute('SELECT * FROM "_vindex"')
results:put(res)
end
local test = tap.test('gh-4231-box-execute-locking')
test:plan(fiber_count)
local exp_metadata = {
{name = 'id', type = 'unsigned'},
{name = 'iid', type = 'unsigned'},
{name = 'name', type = 'string'},
{name = 'type', type = 'string'},
{name = 'opts', type = 'map'},
{name = 'parts', type = 'array'},
}
for _ = 1, fiber_count do
fiber.create(select_from_vindex)
end
for i = 1, fiber_count do
local res = results:get()
test:test(('result %d'):format(i), function(test)
test:plan(2)
test:is_deeply(res.metadata, exp_metadata, 'verify metadata')
local rows_is_ok = type(res.rows) == 'table' and #res.rows > 0
test:ok(rows_is_ok, 'verify rows')
end)
end
os.exit(test:check() and 0 or 1)
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment