Calling box.cfg{} more than once does not normally cause any errors
(even though it might not have any effect). In contrast, assigning
it to some variable and then using it after the box was configured
caused an error since the method was overwritten by the initial call
of <load_cfg>.
 
The patch fixes this issue making box.cfg behave consistently in both
scenarios and is a follow-up for box: make box.execute idempotent function.
 
Follow-up #4231
---
Issue:
https://github.com/tarantool/tarantool/issues/4231 
Branch:
https://github.com/tarantool/tarantool/compare/eljashm/gh-4231-box.execute-immutable-function 
 
 src/box/lua/load_cfg.lua                      | 12 ++++---
 .../gh-4231-immutable-box-execute.test.lua    | 34 ++++++++++++-------
 2 files changed, 30 insertions(+), 16 deletions(-)
diff --git a/src/box/lua/load_cfg.lua b/src/box/lua/load_cfg.lua
index dc4293bdd..6753be6f3 100644
--- a/src/box/lua/load_cfg.lua
+++ b/src/box/lua/load_cfg.lua
@@ -523,7 +523,15 @@ setmetatable(box, {
      end
 })
 
+local function box_is_configured()
+    return ffi.C.box_is_configured()
+end
+
 local function load_cfg(cfg)
+    if box_is_configured() then
+        reload_cfg(cfg)
+        return
+    end
     cfg = upgrade_cfg(cfg, translate_cfg)
     cfg = prepare_cfg(cfg, default_cfg, template_cfg, modify_cfg)
     apply_default_cfg(cfg, default_cfg);
@@ -564,10 +572,6 @@ local function load_cfg(cfg)
 end
 box.cfg = locked(load_cfg)
 
-local function box_is_configured()
-    return ffi.C.box_is_configured()
-end
-
 --
 -- box.execute is <box_load_and_execute> when box is not loaded,
 -- <lbox_execute> otherwise. See also <box_configured> variable.
diff --git a/test/box-tap/gh-4231-immutable-box-execute.test.lua b/test/box-tap/gh-4231-immutable-box-execute.test.lua
index 1544591bf..49aad154b 100755
--- a/test/box-tap/gh-4231-immutable-box-execute.test.lua
+++ b/test/box-tap/gh-4231-immutable-box-execute.test.lua
@@ -1,28 +1,38 @@
 #!/usr/bin/env tarantool
 local tap = require('tap')
 local test = tap.test('execute')
-test:plan(2)
+test:plan(4)
 
 --
--- gh-4231: box.execute should be an idempotent function
--- meaning its effect should be the same if a user chooses
--- to use it before explicit box.cfg invocation
+-- 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.
+-- Within the scope of the same issue box.cfg method should also
+-- be kept idempotent for the same reasons.
 --
 
-local function execute_is_immutable(execute, cmd, msg)
-    local status, err = pcall(execute, cmd)
-    test:ok(status and type(err) == 'table', msg)
-end
-
 local box_execute_stub = box.execute
--- explicit call to load_cfg
+local box_cfg_stub = box.cfg
+
+-- Explicit box configuration that used to change the behavior.
 box.cfg{}
+
 local box_execute_actual = box.execute
+local box_cfg_actual = box.cfg
 
-execute_is_immutable(box_execute_stub,
+local function is_idempotent(method, cmd, msg)
+    local status, err = pcall(method, cmd)
+    test:ok(status, msg, nil, err)
+end
+
+is_idempotent(box_execute_stub,
     "CREATE TABLE t1 (s1 INTEGER, PRIMARY KEY (s1));",
     "box.execute stub works before box.cfg")
-execute_is_immutable(box_execute_actual, "DROP TABLE t1",
+is_idempotent(box_execute_actual, "DROP TABLE t1",
     "box.execute works properly after box.cfg")
 
+is_idempotent(box_cfg_stub, nil, "box.cfg stub works properly")
+is_idempotent(box_cfg_actual, nil, "box.cfg after configuration")
+
 os.exit(test:check() and 0 or 1)
-- 
2.24.0
 
Четверг, 20 февраля 2020, 20:51 +03:00 от Alexander Turenko <alexander.turenko@tarantool.org>:
 
Sorry for the late response.

See comments below.

While looking into the patch, experimenting and commenting I made the
patch (it is based on your changes), so I'll paste it at end of the
email for the reference.

WBR, Alexander Turenko.

> commit 573318ea8749931741247f181333ab68b43a82c6
> Author: Maria <maria.khaydich@tarantool.org>
> Date: Tue Oct 29 17:56:51 2019 +0300
>
> box: box.execute should be immutable function

Now I recalled the right term: idempotent.

Nit: it would be good to use imperative mood in the commit header:

 | Use the imperative mood in the subject line. A properly formed Git
 | commit subject line should always be able to complete the following
 | sentence: “If applied, this commit will /your subject line here/”.

https://www.tarantool.io/en/doc/1.10/dev_guide/developer_guidelines/#how-to-write-a-commit-message

>
> Using box.execute method before explicitly configuring box
> automatically invoked box.cfg nonetheless. Any further calls
> to box.cfg caused its reconfiguration which then led to an
> error when trying to use execute method again.

Sorry that I nitpick around wording again, but after the description I
would check something like the following:

 | box.execute('SELECT 1') -- here box.cfg{} is called under hood
 | box.execute('SELECT 1') -- everything looks ok

After that I would be confused, because everything works as expected.

I would say that there are two different functions
<box_load_and_execute> (which is assigned to box.execute when box is not
configured) and <lbox_execute> (after box confguration) and would
describe the case using those terms. I guess it would easier to
understand.

See also the comment about the test at very end of the email.

>
> The patch introduces a fix making box.execute immutable.
>
> Closes #4231
>
> diff --git a/src/box/lua/load_cfg.lua b/src/box/lua/load_cfg.lua
> index 85617c8f0..1f9b4392f 100644
> --- a/src/box/lua/load_cfg.lua
> +++ b/src/box/lua/load_cfg.lua
> @@ -517,10 +517,14 @@ setmetatable(box, {
> end
> })
>
> +-- Flag needed to keep immutable functions after box reconfiguration

Nit: over 66 symbols.

> +local box_loaded = false
> +
> local function load_cfg(cfg)
> cfg = upgrade_cfg(cfg, translate_cfg)
> cfg = prepare_cfg(cfg, default_cfg, template_cfg, modify_cfg)
> apply_default_cfg(cfg, default_cfg);
> + box_loaded = true

After this we can failed to configure box, consider the case:

 | tarantool> box.cfg{listen = 'incorrect'}
 | ---
 | - error: 'Incorrect value for option ''listen'': expected host:service or /unix.socket'
 | ...
 |
 | tarantool> box.execute('SELECT 1')

We should set the flag only at successful box configuration. I would
also add such test case.

> -- Save new box.cfg
> box.cfg = cfg
> if not pcall(private.cfg_check) then
> @@ -564,7 +568,16 @@ box.cfg = locked(load_cfg)
> -- metatable.
> --
> function box.execute(...)
> - load_cfg()
> + --
> + -- Saving box.execute method before explicit calls to either
> + -- box.execute() or box.cfg{} leads to implicit invocation of
> + -- box.cfg nonetheless.

Technically saving of box.execute does not lead to invocation of
box.cfg. But anyway I would describe things in the way as proposed
above.

> + -- The flag box_loaded makes sure execute
> + -- is an immutable function meaning it won't be overwritten by
> + -- any following attempts to reconfigure box.
> + --

The problem is not that <lbox_execute> will be overwritten with some
<box_another_execute>, but that the initial <box_load_and_execute> does
not work correctly after successful box configuration.

> + if not box_loaded then
> + load_cfg()
> + end

I considered using of `type(box.cfg) == 'function'` check as in
tarantoolctl, but in fact it is not realiable: if box was not configured
after box.cfg() due to an error (say, after `box.cfg{listen =
'invalid'}`) the type of box.cfg will be 'table' and reload_cfg() will
work on the next box.cfg() call. So we should use box_is_configured C
function:

 | local ffi = require('ffi')
 |
 | ffi.cdef([[
 | bool
 | box_is_configured(void);
 | ]])
 |
 | local function box_is_configured()
 | return ffi.C.box_is_configured()
 | end

This way requires to add the function into extra/exports.

BTW, it seems that it is possible that <box_load_and_execute> will be
called when `box.cfg{}` is already in progress in another fiber: this is
why all load_cfg() / reload_cfg() calls are decorated using `locked`
wrapper. It seems we should do the same in <box_load_and_execute>:

 | function box.execute(...)
 | if not box_is_configured() then
 | locked(function()
 | -- Re-check, because after the lock release the box
 | -- state may be changed. We should call load_cfg()
 | -- only once.
 | if not box_is_configured() then
 | load_cfg()
 | end
 | end)()
 | end
 | return box.execute(...)
 | end

I experimented with this like so:

 | $ ./src/tarantool
 | tarantool> box_execute = box.execute
 | tarantool> for i = 1, 10 do require('fiber').create(function() require('log').info(require('yaml').encode((box_execute('SELECT * FROM "_vindex"')))) end) end

While looking into the code I found one more case:

 | tarantool> box_cfg = box.cfg
 | ---
 | ...
 | tarantool> box_cfg()
 | <...>
 | tarantool> box_cfg()
 | ---
 | - error: 'builtin/box/load_cfg.lua:19: builtin/box/load_cfg.lua:547: bad argument
 | #1 to ''pairs'' (table expected, got nil)'
 | ...

I think it does NOT mean that we should not fix box.execute() code,
because the expected behaviour is a bit different:

* box.execute() (more specifically <box_load_and_execute>) should
  configure box if it is not configured or just execute otherwise (w/o
  box reconfiguration).
* box.cfg() (more specifically <load_cfg>) should configure box if it is
  not configured, otherwise **reconfigure** it.

So box.cfg() should call reload_cfg() if box is already configured. I
propose to fix it in a separate patch (but within the scope of the
issue).

> return box.execute(...)
> end
>
> diff --git a/test/box-tap/gh-4231-immutable-box-execute.test.lua b/test/box-tap/gh-4231-immutable-box-execute.test.lua
> new file mode 100755
> index 000000000..5bdbec88f
> --- /dev/null
> +++ b/test/box-tap/gh-4231-immutable-box-execute.test.lua
> @@ -0,0 +1,33 @@
> +#!/usr/bin/env tarantool
> +local tap = require('tap')
> +local test = tap.test('execute')
> +test:plan(3)
> +
> +--
> +-- gh-4231: box.execute should be immutable function meaning it's
> +-- address doesn't change after first box.cfg implicit invocation

But it is not so even after the patch:

 | tarantool> box.execute
 | ---
 | - 'function: 0x4074bc30'
 | ...
 | tarantool> box.execute('SELECT 1')
 | <...>
 | tarantool> box.execute
 | ---
 | - 'function: 0x416b2600'
 | ...

> +--
> +
> +local function execute_is_immutable(execute, cmd, msg)
> + local status, err = pcall(function()
> + execute(cmd)
> + end)

Nit: Can be written as:

 | local status, err = pcall(execute, cmd)

> + test:ok(status and err == nil, msg)
> +end
> +
> +local box_execute_stub = box.execute
> +execute_is_immutable(box_execute_stub, "SELECT 1",
> + "execute does not work properly before box.cfg")

This will print:

ok - execute does not work properly before box.cfg

--that looks counter-intuitive for me.

BTW, it looks as the preparation code for the test rather than the test
itself. Personally I think that such code should do assert() rather than
produce 'ok/not ok' TAP13 output. This is not the strict requirement,
however.

Consider [1], from words 'I would split test preparation / clean up code
from actual test cases'.

[1]: https://lists.tarantool.org/pipermail/tarantool-patches/2020-January/013595.html

> +
> +local box_execute_actual = box.execute
> +-- explicit call to load_cfg
> +box.cfg{}
> +
> +-- checking the function was not reconfigured, i.e. adress stays the same

Typo: adress -> address.

> +test:is(box_execute_actual, box.execute,
> + "execute is not the same after box.cfg")
> +execute_is_immutable(box_execute_actual,
> + "CREATE TABLE t1 (s1 INTEGER, PRIMARY KEY (s1));",
> + "execute does not work properly after box.cfg")
> +
> +os.exit(test:check() and 0 or 1)

The test case works properly (does not fail) on current master w/o the
fix. I guess you intend to test box_execute_stub after box.cfg().

BTW, I suggest to run a test on a tarantool version before a fix to
verify that the test actually able to catch the problem.

----

diff --git a/extra/exports b/extra/exports
index 7b84a1452..8599614ee 100644
--- a/extra/exports
+++ b/extra/exports
@@ -118,6 +118,8 @@ swim_member_unref
 swim_member_is_dropped
 swim_member_is_payload_up_to_date
 
+box_is_configured
+
 # Module API
 
 _say
diff --git a/src/box/lua/load_cfg.lua b/src/box/lua/load_cfg.lua
index 1f9b4392f..dfff772d7 100644
--- a/src/box/lua/load_cfg.lua
+++ b/src/box/lua/load_cfg.lua
@@ -6,6 +6,12 @@ local private = require('box.internal')
 local urilib = require('uri')
 local math = require('math')
 local fiber = require('fiber')
+local ffi = require('ffi')
+
+ffi.cdef([[
+ bool
+ box_is_configured(void);
+]])
 
 -- Function decorator that is used to prevent box.cfg() from
 -- being called concurrently by different fibers.
@@ -517,14 +523,10 @@ setmetatable(box, {
      end
 })
 
--- Flag needed to keep immutable functions after box reconfiguration
-local box_loaded = false
-
 local function load_cfg(cfg)
     cfg = upgrade_cfg(cfg, translate_cfg)
     cfg = prepare_cfg(cfg, default_cfg, template_cfg, modify_cfg)
     apply_default_cfg(cfg, default_cfg);
- box_loaded = true
     -- Save new box.cfg
     box.cfg = cfg
     if not pcall(private.cfg_check) then
@@ -562,24 +564,43 @@ local function load_cfg(cfg)
 end
 box.cfg = locked(load_cfg)
 
+local function box_is_configured()
+ return ffi.C.box_is_configured()
+end
+
 --
--- This makes possible do box.execute without calling box.cfg
--- manually. The load_cfg call overwrites following table and
--- metatable.
+-- box.execute is <box_load_and_execute> when box is not loaded,
+-- <lbox_execute> otherwise. See also <box_configured> variable.
 --
-function box.execute(...)
- --
- -- Saving box.execute method before explicit calls to either
- -- box.execute() or box.cfg{} leads to implicit invocation of
- -- box.cfg nonetheless. The flag box_loaded makes sure execute
- -- is an immutable function meaning it won't be overwritten by
- -- any following attempts to reconfigure box.
+-- <box_load_and_execute> loads box and calls <lbox_execute>.
+--
+local box_load_and_execute
+box_load_and_execute = function(...)
+ -- Configure box if it is not configured, no-op otherwise (not
+ -- reconfiguration).
     --
- if not box_loaded then
- load_cfg()
+ -- 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
 
 
--
Maria Khaydich