Tarantool development patches archive
 help / color / mirror / Atom feed
From: "Maria Khaydich" <maria.khaydich@tarantool.org>
To: "Alexander Turenko" <alexander.turenko@tarantool.org>
Cc: tarantool-patches@dev.tarantool.org
Subject: Re: [Tarantool-patches] [PATCH] box: make box.execute() immutable
Date: Wed, 11 Mar 2020 18:56:14 +0300	[thread overview]
Message-ID: <1583942174.197473001@f388.i.mail.ru> (raw)
In-Reply-To: <20200220175124.pwj24lx3jz4w6zcm@tkn_work_nb>

[-- Attachment #1: Type: text/plain, Size: 16600 bytes --]


Thank you for the review! I reworked the patch:
----------------------------------------------------------------------
Saving box.execute method before explicitly configuring box
automatically invokes load_cfg() nonetheless. Further calls
to box.cfg{} caused its reconfiguration and replacement of
previously saved box.execute meaning it couldn't be used
again without leading to an error.
 
The patch introduces a workaround making box.execute idempotent
and is heavily influenced by @Totktonada.
 
Closes #4231
---
Issue:
https://github.com/tarantool/tarantool/issues/4231  
Branch:
https://github.com/tarantool/tarantool/compare/eljashm/gh-4231-box.execute-immutable-function  
 
 extra/exports                                 |  2 +
 src/box/lua/load_cfg.lua                      | 42 ++++++++++++++++---
 .../gh-4231-immutable-box-execute.test.lua    | 28 +++++++++++++
 3 files changed, 67 insertions(+), 5 deletions(-)
 create mode 100755 test/box-tap/gh-4231-immutable-box-execute.test.lua
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 85617c8f0..dc4293bdd 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.
@@ -558,15 +564,41 @@ 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.
+-- <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
+    -- 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
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..1544591bf
--- /dev/null
+++ b/test/box-tap/gh-4231-immutable-box-execute.test.lua
@@ -0,0 +1,28 @@
+#!/usr/bin/env tarantool
+local tap = require('tap')
+local test = tap.test('execute')
+test:plan(2)
+
+--
+-- 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
+--
+
+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
+box.cfg{}
+local box_execute_actual = box.execute
+
+execute_is_immutable(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",
+    "box.execute works properly after box.cfg")
+
+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
 

[-- Attachment #2: Type: text/html, Size: 20249 bytes --]

  parent reply	other threads:[~2020-03-11 15:56 UTC|newest]

Thread overview: 31+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2019-11-14 11:50 [Tarantool-patches] [PATCH] box.execute should be immutable function Maria
2019-11-14 16:51 ` Nikita Pettik
2019-12-17 14:39 ` Igor Munkin
2019-12-24 15:32   ` [Tarantool-patches] [PATCH] box: make box.execute() immutable Maria Khaydich
2019-12-25  1:30     ` Igor Munkin
2019-12-26 14:08     ` Alexander Turenko
2020-01-13 12:13       ` Maria Khaydich
2020-01-13 15:48         ` Igor Munkin
2020-01-18 10:56           ` Maria Khaydich
2020-02-20 17:51             ` Alexander Turenko
2020-02-20 21:15               ` Igor Munkin
2020-03-11 15:56               ` Maria Khaydich [this message]
2020-03-18 22:25                 ` Igor Munkin
2020-05-02 14:52                   ` Alexander Turenko
2020-05-12 16:16                 ` Alexander Turenko
2020-03-11 15:57               ` [Tarantool-patches] [PATCH 1/2] box: make box.cfg idempotent function Maria Khaydich
2020-03-12 13:29                 ` Konstantin Osipov
2020-03-12 19:25                   ` Maria Khaydich
2020-03-12 20:00                     ` Konstantin Osipov
2020-03-18 22:26                       ` Igor Munkin
2020-03-19  7:19                         ` Konstantin Osipov
2020-03-19  9:08                           ` Igor Munkin
2020-03-19 10:06                             ` Konstantin Osipov
2020-03-19 10:26                               ` Igor Munkin
2020-05-06 11:17                           ` Alexander Turenko
2020-05-06 11:49                             ` Konstantin Osipov
2020-05-06 12:53                               ` Alexander Turenko
2020-05-06 13:02                                 ` Konstantin Osipov
2020-05-06 13:13                                   ` Alexander Turenko
2020-03-18 22:26                 ` Igor Munkin
2020-05-12 16:17                   ` Alexander Turenko

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=1583942174.197473001@f388.i.mail.ru \
    --to=maria.khaydich@tarantool.org \
    --cc=alexander.turenko@tarantool.org \
    --cc=tarantool-patches@dev.tarantool.org \
    --subject='Re: [Tarantool-patches] [PATCH] box: make box.execute() immutable' \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox