From 9d1cbda5379319c6e49ef878426767a125c20f21 Mon Sep 17 00:00:00 2001
From: Aleksandr Lyapunov <alyapunov@tarantool.org>
Date: Wed, 28 Dec 2022 14:29:17 +0300
Subject: [PATCH] box: introduce options in box.atomic

If the first argument of box.atomic is a non-callable table then
consider it as options table for box.begin{}.

For test and debug purposes introduce internal getter of current
transaction isolation level as box.internal.txn_isolation().

Closes #7202

@TarantoolBot document
Title: Options in box.atomic

Now it's allowed to pass transaction options in the first argument
of box.atomic(..) call. The options must be a table, exactly as
in box.begin(..). If options are passed as the first arguments,
the second and the rest arguments are expected to be a functions
and its arguments, like in usual box.atomic.
---
 .../unreleased/gh-7202-options-in-atomic.md   |  3 +
 extra/exports                                 |  1 +
 src/box/lua/schema.lua                        | 28 ++++++-
 src/box/txn.c                                 | 10 +++
 src/box/txn.h                                 |  8 ++
 .../gh_7202_options_in_atomic_test.lua        | 77 +++++++++++++++++++
 6 files changed, 124 insertions(+), 3 deletions(-)
 create mode 100644 changelogs/unreleased/gh-7202-options-in-atomic.md
 create mode 100644 test/box-luatest/gh_7202_options_in_atomic_test.lua

diff --git a/changelogs/unreleased/gh-7202-options-in-atomic.md b/changelogs/unreleased/gh-7202-options-in-atomic.md
new file mode 100644
index 0000000000..d945ece569
--- /dev/null
+++ b/changelogs/unreleased/gh-7202-options-in-atomic.md
@@ -0,0 +1,3 @@
+## feature/box
+
+* Introduced transaction options in box.atomic() by analogy with box.begin() (gh-7202).
diff --git a/extra/exports b/extra/exports
index 26b64186a9..4693586ddd 100644
--- a/extra/exports
+++ b/extra/exports
@@ -140,6 +140,7 @@ box_txn_alloc
 box_txn_begin
 box_txn_commit
 box_txn_id
+box_txn_isolation
 box_txn_rollback
 box_txn_rollback_to_savepoint
 box_txn_savepoint
diff --git a/src/box/lua/schema.lua b/src/box/lua/schema.lua
index 30a6244934..70db43f296 100644
--- a/src/box/lua/schema.lua
+++ b/src/box/lua/schema.lua
@@ -72,6 +72,8 @@ ffi.cdef[[
     int64_t
     box_txn_id();
     int
+    box_txn_isolation();
+    int
     box_txn_begin();
     int
     box_txn_set_timeout(double timeout);
@@ -459,6 +461,11 @@ box.txn_id = function()
     return tonumber(id)
 end
 
+box.internal.txn_isolation = function()
+    local lvl = builtin.box_txn_isolation()
+    return lvl ~= -1 and lvl or nil
+end
+
 box.savepoint = function()
     local csavepoint = builtin.box_txn_savepoint()
     if csavepoint == nil then
@@ -476,9 +483,24 @@ local function atomic_tail(status, ...)
      return ...
 end
 
-box.atomic = function(fun, ...)
-    box.begin()
-    return atomic_tail(pcall(fun, ...))
+box.atomic = function(arg0, arg1, ...)
+    -- There are two cases:
+    -- 1. arg0 is a function (callable in general) while arg1,... are arguments.
+    -- 2. arg0 is an options table, arg1 - a function and ... are arguments.
+    -- The simplest way to distinguish that cases (without any other checks
+    -- for correctness) is to check that arg0 is not a callable table.
+    local arg0_is_noncallable_table = false
+    if type(arg0) == 'table' then
+        local mt = debug.getmetatable(arg0)
+        arg0_is_noncallable_table = mt == nil or mt.__call == nil
+    end
+    if arg0_is_noncallable_table then
+        box.begin(arg0)
+        return atomic_tail(pcall(arg1, ...))
+    else
+        box.begin()
+        return atomic_tail(pcall(arg0, arg1, ...))
+    end
 end
 
 -- box.commit yields, so it's defined as Lua/C binding
diff --git a/src/box/txn.c b/src/box/txn.c
index 2ba8350589..75dacff390 100644
--- a/src/box/txn.c
+++ b/src/box/txn.c
@@ -1220,6 +1220,16 @@ box_txn_id(void)
 		return -1;
 }
 
+int
+box_txn_isolation(void)
+{
+	struct txn *txn = in_txn();
+	if (txn != NULL)
+		return txn->isolation;
+	else
+		return -1;
+}
+
 bool
 box_txn(void)
 {
diff --git a/src/box/txn.h b/src/box/txn.h
index 5aecfd55dc..c6d679073b 100644
--- a/src/box/txn.h
+++ b/src/box/txn.h
@@ -963,6 +963,14 @@ tx_region_release(struct txn *txn, enum tx_alloc_type alloc_type);
 API_EXPORT int64_t
 box_txn_id(void);
 
+/**
+ * Get isolation level of current transaction, one of enum txn_isolation_level
+ * values (but cannot be TXN_ISOLATION_DEFAULT (which is zero) by design).
+ * -1 if there is no current transaction.
+ */
+API_EXPORT int
+box_txn_isolation(void);
+
 /**
  * Return true if there is an active transaction.
  */
diff --git a/test/box-luatest/gh_7202_options_in_atomic_test.lua b/test/box-luatest/gh_7202_options_in_atomic_test.lua
new file mode 100644
index 0000000000..01288bca8b
--- /dev/null
+++ b/test/box-luatest/gh_7202_options_in_atomic_test.lua
@@ -0,0 +1,77 @@
+local server = require('luatest.server')
+local t = require('luatest')
+
+local g = t.group()
+
+g.before_all = function()
+    g.server = server:new{
+        alias  = 'default',
+        box_cfg = {
+            txn_isolation = 'best-effort',
+        }
+    }
+    g.server:start()
+end
+
+g.after_all = function()
+    g.server:drop()
+end
+
+-- Test of box.atomic with or without options.
+g.test_atomic_options = function()
+    g.server:exec(function()
+        local lt = require('luatest')
+
+        -- Simple function
+        local function f(...)
+            return box.internal.txn_isolation(), ...
+        end
+        -- Simple callable table
+        local t = setmetatable({'table'}, {__call = f})
+
+        -- Atomic without options
+        lt.assert_equals({box.atomic(f, 1, 2)},
+            {box.txn_isolation_level.BEST_EFFORT, 1, 2})
+        lt.assert_equals({box.atomic(t, 1, 2)},
+            {box.txn_isolation_level.BEST_EFFORT, {'table'}, 1, 2})
+
+        -- Atomic with empty options
+        lt.assert_equals({box.atomic({}, f, 1, 2)},
+            {box.txn_isolation_level.BEST_EFFORT, 1, 2})
+        lt.assert_equals({box.atomic({}, t, 1, 2)},
+            {box.txn_isolation_level.BEST_EFFORT, {'table'}, 1, 2})
+
+        -- Atomic with options
+        local opts = {txn_isolation = 'read-committed'}
+        lt.assert_equals({box.atomic(opts, f, 1, 2)},
+            {box.txn_isolation_level.READ_COMMITTED, 1, 2})
+        lt.assert_equals({box.atomic(opts, t, 1, 2)},
+            {box.txn_isolation_level.READ_COMMITTED, {'table'}, 1, 2})
+
+        -- Different invalid options
+        lt.assert_error_msg_equals("Illegal parameters, unexpected option 'aa'",
+            box.atomic, {aa = 'bb'}, f)
+
+        lt.assert_error_msg_equals("Illegal parameters, unexpected option 'aa'",
+            box.atomic, {txn_isolation = 0, aa = 'bb'}, f)
+
+        lt.assert_error_msg_equals(
+            "Illegal parameters, " ..
+            "txn_isolation must be one of box.txn_isolation_level " ..
+            "(keys or values)",
+            box.atomic, {txn_isolation = 'wtf'}, f)
+
+        lt.assert_error_msg_contains(
+            "attempt to call a nil value",
+            box.atomic, {})
+
+        -- Different invalid function objects
+        lt.assert_error_msg_contains(
+            "attempt to call a table value",
+            box.atomic, {}, {})
+
+        lt.assert_error_msg_contains(
+            "attempt to call a nil value",
+            box.atomic, {})
+    end)
+end
-- 
GitLab