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