From ec750af66d9497207b358d0bc4a465163283f7ab Mon Sep 17 00:00:00 2001
From: Aleksandr Lyapunov <alyapunov@tarantool.org>
Date: Fri, 18 Mar 2022 10:02:21 +0300
Subject: [PATCH] txm: introduce transaction isolation levels

Now memtx TX manager tries to determine the best isolation level
by itself. There could be two options:
* READ_COMMITTED, when the transaction see changes of other tx
that are committed but not yet confirmed (written to WAL)
* READ_CONFIRMED, when the transaction see only confirmed changes.

Introduce a simple way to specify the isolation level explicitly:
box.begin{tx_isolation = 'default'} - the same as box.begin().
box.begin{tx_isolation = 'read-committed'} - READ_COMMITTED.
box.begin{tx_isolation = 'read-confirmed'} - READ_CONFIRMED.
box.begin{tx_isolation = 'best-effort'} - old automatic way.

Intrduce a bit more complex but potentially faster way to set
isolation level, like that:
my_level = box.tnx_isolation_level.READ_COMMITTED
..
box.begin{tx_isolation = my_level}

For simplicity of implementation also support symmetric values as
'READ_COMMITTED' and box.tnx_isolation_level['read-committed'].

Introduce a new box.cfg option - default_tx_isolation, that is
used as a default when a transaction is started. The option is
dynamic and possible values are the same as in box.begin, except
'default' which is meaningless.

In addition to string value the corresponding numeric values can
be used in both box.begin and box.cfg.

Part of #6930
NO_DOC=see later commits
NO_CHANGELOG=see later commits
---
 extra/exports                                 |   1 +
 src/box/box.cc                                |  46 +++++
 src/box/box.h                                 |   6 +
 src/box/lua/cfg.cc                            |   9 +
 src/box/lua/load_cfg.lua                      |   4 +
 src/box/lua/schema.lua                        |  51 +++++
 src/box/memtx_tx.c                            |   9 +-
 src/box/txn.c                                 |  40 ++++
 src/box/txn.h                                 |  61 ++++++
 test/app-tap/init_script.result               |   1 +
 .../gh_6930_mvcc_isolation_levels_test.lua    | 176 ++++++++++++++++++
 test/box/admin.result                         |   2 +
 test/box/cfg.result                           |   4 +
 test/box/misc.result                          |   1 +
 14 files changed, 410 insertions(+), 1 deletion(-)
 create mode 100644 test/box-luatest/gh_6930_mvcc_isolation_levels_test.lua

diff --git a/extra/exports b/extra/exports
index efbe17eef2..7456b584cd 100644
--- a/extra/exports
+++ b/extra/exports
@@ -100,6 +100,7 @@ box_txn_id
 box_txn_rollback
 box_txn_rollback_to_savepoint
 box_txn_savepoint
+box_txn_set_isolation
 box_txn_set_timeout
 box_update
 box_upsert
diff --git a/src/box/box.cc b/src/box/box.cc
index 675dc60104..814b1779ca 100644
--- a/src/box/box.cc
+++ b/src/box/box.cc
@@ -1263,6 +1263,40 @@ box_check_txn_timeout(void)
 	return timeout;
 }
 
+/**
+ * Get and check isolation level from config, converting number or string to
+ * enum txn_isolation_level.
+ * @return isolation level or txn_isolation_level_MAX is case of error.
+ */
+static enum txn_isolation_level
+box_check_txn_isolation(void)
+{
+	uint32_t level;
+	if (cfg_isnumber("txn_isolation")) {
+		level = cfg_geti("txn_isolation");
+	} else {
+		const char *str_level = cfg_gets("txn_isolation");
+		level = strindex(txn_isolation_level_strs, str_level,
+				 txn_isolation_level_MAX);
+		if (level == txn_isolation_level_MAX)
+			level = strindex(txn_isolation_level_aliases, str_level,
+					 txn_isolation_level_MAX);
+	}
+	if (level >= txn_isolation_level_MAX) {
+		diag_set(ClientError, ER_CFG, "txn_isolation",
+			 "must be one of "
+			 "box.txn_isolation_level (keys or values)");
+		return txn_isolation_level_MAX;
+	}
+	if (level == TXN_ISOLATION_DEFAULT) {
+		diag_set(ClientError, ER_CFG, "txn_isolation",
+			 "cannot set default transaction isolation "
+			 "to 'default'");
+		return txn_isolation_level_MAX;
+	}
+	return (enum txn_isolation_level)level;
+}
+
 void
 box_check_config(void)
 {
@@ -1310,6 +1344,8 @@ box_check_config(void)
 		diag_raise();
 	if (box_check_txn_timeout() < 0)
 		diag_raise();
+	if (box_check_txn_isolation() == txn_isolation_level_MAX)
+		diag_raise();
 }
 
 int
@@ -2174,6 +2210,16 @@ box_set_txn_timeout(void)
 	return 0;
 }
 
+int
+box_set_txn_isolation(void)
+{
+	enum txn_isolation_level level = box_check_txn_isolation();
+	if (level == txn_isolation_level_MAX)
+		return -1;
+	txn_default_isolation = level;
+	return 0;
+}
+
 /* }}} configuration bindings */
 
 /**
diff --git a/src/box/box.h b/src/box/box.h
index 2338e6bf24..b53a0aad97 100644
--- a/src/box/box.h
+++ b/src/box/box.h
@@ -279,6 +279,12 @@ void box_set_replication_anon(void);
 void box_set_net_msg_max(void);
 int box_set_crash(void);
 int box_set_txn_timeout(void);
+/**
+ * Set default isolation level from cfg option txn_isolation.
+ * @return 0 on success, -1 on error.
+ */
+int
+box_set_txn_isolation(void);
 
 int
 box_set_prepared_stmt_cache_size(void);
diff --git a/src/box/lua/cfg.cc b/src/box/lua/cfg.cc
index 5ea23917d3..b3f2f333c7 100644
--- a/src/box/lua/cfg.cc
+++ b/src/box/lua/cfg.cc
@@ -404,6 +404,14 @@ lbox_cfg_set_txn_timeout(struct lua_State *L)
 	return 0;
 }
 
+static int
+lbox_cfg_set_txn_isolation(struct lua_State *L)
+{
+	if (box_set_txn_isolation() != 0)
+		luaT_error(L);
+	return 0;
+}
+
 void
 box_lua_cfg_init(struct lua_State *L)
 {
@@ -444,6 +452,7 @@ box_lua_cfg_init(struct lua_State *L)
 		{"cfg_set_sql_cache_size", lbox_set_prepared_stmt_cache_size},
 		{"cfg_set_crash", lbox_cfg_set_crash},
 		{"cfg_set_txn_timeout", lbox_cfg_set_txn_timeout},
+		{"cfg_set_txn_isolation", lbox_cfg_set_txn_isolation},
 		{NULL, NULL}
 	};
 
diff --git a/src/box/lua/load_cfg.lua b/src/box/lua/load_cfg.lua
index 7bc2f83bf3..7d86dd1b28 100644
--- a/src/box/lua/load_cfg.lua
+++ b/src/box/lua/load_cfg.lua
@@ -115,6 +115,7 @@ local default_cfg = {
     net_msg_max           = 768,
     sql_cache_size        = 5 * 1024 * 1024,
     txn_timeout           = 365 * 100 * 86400,
+    txn_isolation         = "best-effort",
 }
 
 -- cfg variables which are covered by modules
@@ -207,6 +208,7 @@ local template_cfg = {
     read_only           = 'boolean',
     hot_standby         = 'boolean',
     memtx_use_mvcc_engine = 'boolean',
+    txn_isolation = 'string, number',
     worker_pool_threads = 'number',
     election_mode       = 'string',
     election_timeout    = 'number',
@@ -336,6 +338,7 @@ local dynamic_cfg = {
     net_msg_max             = private.cfg_set_net_msg_max,
     sql_cache_size          = private.cfg_set_sql_cache_size,
     txn_timeout             = private.cfg_set_txn_timeout,
+    txn_isolation           = private.cfg_set_txn_isolation,
 }
 
 -- dynamically settable options, which should be reverted in case
@@ -670,6 +673,7 @@ local box_cfg_guard_whitelist = {
     ctl = true;
     watch = true;
     broadcast = true;
+    txn_isolation_level = true;
     NULL = true;
 };
 
diff --git a/src/box/lua/schema.lua b/src/box/lua/schema.lua
index 154e98ed71..a035155243 100644
--- a/src/box/lua/schema.lua
+++ b/src/box/lua/schema.lua
@@ -73,6 +73,8 @@ ffi.cdef[[
     box_txn_begin();
     int
     box_txn_set_timeout(double timeout);
+    int
+    box_txn_set_isolation(uint32_t level);
     /** \endcond public */
     /** \cond public */
     int
@@ -336,8 +338,48 @@ local function feedback_save_event(event)
     end
 end
 
+-- Public isolation level map string -> number.
+box.txn_isolation_level = {
+    ['default'] = 0,
+    ['DEFAULT'] = 0,
+    ['read-committed'] = 1,
+    ['READ_COMMITTED'] = 1,
+    ['read-confirmed'] = 2,
+    ['READ_CONFIRMED'] = 2,
+    ['best-effort'] = 3,
+    ['BEST_EFFORT'] = 3,
+}
+
+-- Create private isolation level map anything-correct -> number.
+local function create_txn_isolation_level_map()
+    local res = {}
+    for k,v in pairs(box.txn_isolation_level) do
+        res[k] = v
+        res[v] = v
+    end
+    return res
+end
+
+-- Private isolation level map anything-correct -> number.
+local txn_isolation_level_map = create_txn_isolation_level_map()
+box.internal.txn_isolation_level_map = txn_isolation_level_map
+
+-- Convert to numeric the value of txn isolation level, raise if failed.
+local function normalize_txn_isolation_level(txn_isolation)
+    txn_isolation = txn_isolation_level_map[txn_isolation]
+    if txn_isolation == nil then
+        box.error(box.error.ILLEGAL_PARAMS,
+                  "txn_isolation must be one of box.txn_isolation_level" ..
+                  " (keys or values)")
+    end
+    return txn_isolation
+end
+
+box.internal.normalize_txn_isolation_level = normalize_txn_isolation_level
+
 box.begin = function(options)
     local timeout
+    local txn_isolation
     if options then
         check_param(options, 'options', 'table')
         timeout = options.timeout
@@ -345,6 +387,10 @@ box.begin = function(options)
             box.error(box.error.ILLEGAL_PARAMS,
                       "timeout must be a number greater than 0")
         end
+        txn_isolation = options.txn_isolation
+        if txn_isolation ~= nil then
+            txn_isolation = normalize_txn_isolation_level(txn_isolation)
+        end
     end
     if builtin.box_txn_begin() == -1 then
         box.error()
@@ -352,6 +398,11 @@ box.begin = function(options)
     if timeout then
         assert(builtin.box_txn_set_timeout(timeout) == 0)
     end
+    if txn_isolation and
+       builtin.box_txn_set_isolation(txn_isolation) ~= 0 then
+        box.rollback()
+        box.error()
+    end
 end
 
 box.is_in_txn = builtin.box_txn
diff --git a/src/box/memtx_tx.c b/src/box/memtx_tx.c
index c8a36fc4c3..3b490790fd 100644
--- a/src/box/memtx_tx.c
+++ b/src/box/memtx_tx.c
@@ -2007,6 +2007,13 @@ memtx_tx_tuple_clarify_impl(struct txn *txn, struct space *space,
 static bool
 detect_whether_prepared_ok(struct txn *txn)
 {
+	if (txn == NULL)
+		return false;
+	else if (txn->isolation == TXN_ISOLATION_READ_COMMITTED)
+		return true;
+	else if (txn->isolation == TXN_ISOLATION_READ_CONFIRMED)
+		return false;
+	assert(txn->isolation == TXN_ISOLATION_BEST_EFFORT);
 	/*
 	 * The best effort that we can make is to determine whether the
 	 * transaction is read-only or not. For read only (including autocommit
@@ -2014,7 +2021,7 @@ detect_whether_prepared_ok(struct txn *txn)
 	 * ignoring prepared. For read-write transaction we should see prepared
 	 * changes in order to avoid conflicts.
 	 */
-	return txn != NULL && !stailq_empty(&txn->stmts);
+	return !stailq_empty(&txn->stmts);
 }
 
 /**
diff --git a/src/box/txn.c b/src/box/txn.c
index fa9cab707e..af009c90b1 100644
--- a/src/box/txn.c
+++ b/src/box/txn.c
@@ -45,6 +45,22 @@ double too_long_threshold;
 /** Last prepare-sequence-number that was assigned to prepared TX. */
 int64_t txn_last_psn = 0;
 
+enum txn_isolation_level txn_default_isolation = TXN_ISOLATION_BEST_EFFORT;
+
+const char *txn_isolation_level_strs[txn_isolation_level_MAX] = {
+	"DEFAULT",
+	"READ_COMMITTED",
+	"READ_CONFIRMED",
+	"BEST_EFFORT",
+};
+
+const char *txn_isolation_level_aliases[txn_isolation_level_MAX] = {
+	"default",
+	"read-committed",
+	"read-confirmed",
+	"best-effort",
+};
+
 /* Txn cache. */
 static struct stailq txn_cache = {NULL, &txn_cache.first};
 
@@ -352,6 +368,7 @@ txn_begin(void)
 	txn->psn = 0;
 	txn->rv_psn = 0;
 	txn->status = TXN_INPROGRESS;
+	txn->isolation = txn_default_isolation;
 	txn->signature = TXN_SIGNATURE_UNKNOWN;
 	txn->engine = NULL;
 	txn->engine_tx = NULL;
@@ -1183,6 +1200,29 @@ box_txn_set_timeout(double timeout)
 	return 0;
 }
 
+int
+box_txn_set_isolation(uint32_t level)
+{
+	if (level >= txn_isolation_level_MAX) {
+		diag_set(ClientError, ER_ILLEGAL_PARAMS,
+			 "unknown isolation level");
+		return -1;
+	}
+	struct txn *txn = in_txn();
+	if (txn == NULL) {
+		diag_set(ClientError, ER_NO_TRANSACTION);
+		return -1;
+	}
+	if (!stailq_empty(&txn->stmts)) {
+		diag_set(ClientError, ER_ACTIVE_TRANSACTION);
+		return -1;
+	}
+	if (level == TXN_ISOLATION_DEFAULT)
+		level = txn_default_isolation;
+	txn->isolation = level;
+	return 0;
+}
+
 struct txn_savepoint *
 txn_savepoint_new(struct txn *txn, const char *name)
 {
diff --git a/src/box/txn.h b/src/box/txn.h
index de57b51d68..491387d02a 100644
--- a/src/box/txn.h
+++ b/src/box/txn.h
@@ -146,6 +146,51 @@ enum {
 	TXN_SIGNATURE_ABORT = JOURNAL_ENTRY_ERR_MIN - 4,
 };
 
+/** \cond public */
+/**
+ * When a transaction calls `commit`, this action can last for some time until
+ * redo data is written to WAL. While such a `commit` call is in progress we
+ * call changes of such a transaction as 'committed', and when the process is
+ * finished - we call the changes as 'confirmed'. One of the main options of
+ * a transaction is to see or not to see 'committed' changes.
+ * Note that now there are different terminologies in different places. This
+ * enum uses new 'committed' and 'confirmed' states of transactions. Meanwhile
+ * in engined the first state is usually called as 'prepared', and the second
+ * as 'committed' or 'completed'.
+ * Warning: this enum is exposed in lua via ffi, and thus any change in items
+ * must be correspondingly modified on ffi.cdef(), see schema.lua.
+ */
+enum txn_isolation_level {
+	/** Take isolation level from global default_isolation_level. */
+	TXN_ISOLATION_DEFAULT,
+	/** Allow to read committed, but not confirmed changes. */
+	TXN_ISOLATION_READ_COMMITTED,
+	/** Allow to read only confirmed changes. */
+	TXN_ISOLATION_READ_CONFIRMED,
+	/** Determine isolation level automatically. */
+	TXN_ISOLATION_BEST_EFFORT,
+	/** Upper bound of valid values. */
+	txn_isolation_level_MAX,
+};
+
+/** \endcond public */
+
+/**
+ * Common enum strings: uppercase letters, underscores.
+ */
+extern const char *txn_isolation_level_strs[txn_isolation_level_MAX];
+
+/**
+ * Aliases: lowercase letters, hyphens.
+ */
+extern const char *txn_isolation_level_aliases[txn_isolation_level_MAX];
+
+/**
+ * The level that is set for a transaction by default.
+ * Cannot be TXN_ISOLATION_DEFAULT since it senseless.
+ */
+extern enum txn_isolation_level txn_default_isolation;
+
 /**
  * Convert a result of a transaction execution to an error installed into the
  * current diag.
@@ -364,6 +409,11 @@ struct txn {
 	int64_t rv_psn;
 	/** Status of the TX */
 	enum txn_status status;
+	/**
+	 * Isolation level of TX. Can't be TXN_ISOLATION_DEFAULT since setting
+	 * this value actually uses txn_default_isolation
+	 */
+	enum txn_isolation_level isolation;
 	/** List of statements in a transaction. */
 	struct stailq stmts;
 	/** Number of new rows without an assigned LSN. */
@@ -880,6 +930,17 @@ box_txn_alloc(size_t size);
 API_EXPORT int
 box_txn_set_timeout(double timeout);
 
+/**
+ * Set an isolation @a level for a transaction.
+ * Must be called before the first DML.
+ * The level must be of enun txn_isolation_level values.
+ * @retval 0 if success
+ * @retval -1 if failed, diag is set.
+ *
+ */
+API_EXPORT int
+box_txn_set_isolation(uint32_t level);
+
 /** \endcond public */
 
 typedef struct txn_savepoint box_txn_savepoint_t;
diff --git a/test/app-tap/init_script.result b/test/app-tap/init_script.result
index b7e2ce09cb..5fae022ea1 100644
--- a/test/app-tap/init_script.result
+++ b/test/app-tap/init_script.result
@@ -46,6 +46,7 @@ slab_alloc_granularity:8
 sql_cache_size:5242880
 strip_core:true
 too_long_threshold:0.5
+txn_isolation:best-effort
 txn_timeout:3153600000
 vinyl_bloom_fpr:0.05
 vinyl_cache:134217728
diff --git a/test/box-luatest/gh_6930_mvcc_isolation_levels_test.lua b/test/box-luatest/gh_6930_mvcc_isolation_levels_test.lua
new file mode 100644
index 0000000000..94b670e95e
--- /dev/null
+++ b/test/box-luatest/gh_6930_mvcc_isolation_levels_test.lua
@@ -0,0 +1,176 @@
+local server = require('test.luatest_helpers.server')
+local t = require('luatest')
+
+local g = t.group()
+
+g.before_all = function()
+    g.server = server:new{
+        alias   = 'default',
+        box_cfg = {memtx_use_mvcc_engine = true}
+    }
+    g.server:start()
+end
+
+g.after_all = function()
+    g.server:drop()
+end
+
+g.test_mvcc_isolation_level_errors = function()
+    g.server:exec(function()
+        local t = require('luatest')
+        t.assert_error_msg_content_equals(
+            "Illegal parameters, txn_isolation must be one of " ..
+            "box.txn_isolation_level (keys or values)",
+            function() box.begin{txn_isolation = 'avadakedavra'} end)
+        t.assert_error_msg_content_equals(
+            "Incorrect value for option 'txn_isolation': must " ..
+            "be one of box.txn_isolation_level (keys or values)",
+            function() box.cfg{txn_isolation = 'avadakedavra'} end)
+        t.assert_error_msg_content_equals(
+            "Illegal parameters, txn_isolation must be one of " ..
+            "box.txn_isolation_level (keys or values)",
+            function() box.begin{txn_isolation = false} end)
+        t.assert_error_msg_content_equals(
+            "Incorrect value for option 'txn_isolation': " ..
+            "should be one of types string, number",
+            function() box.cfg{txn_isolation = false} end)
+        t.assert_error_msg_content_equals(
+            "Illegal parameters, txn_isolation must be one of " ..
+            "box.txn_isolation_level (keys or values)",
+            function() box.begin{txn_isolation = 8} end)
+        t.assert_error_msg_content_equals(
+            "Incorrect value for option 'txn_isolation': must " ..
+            "be one of box.txn_isolation_level (keys or values)",
+            function() box.cfg{txn_isolation = 8} end)
+        t.assert_error_msg_content_equals(
+            "Incorrect value for option 'txn_isolation': " ..
+            "cannot set default transaction isolation to 'default'",
+            function() box.cfg{txn_isolation = 'default'} end)
+    end)
+end
+
+g.before_test('test_mvcc_isolation_level_basics', function()
+    g.server:exec(function()
+        local s = box.schema.space.create('test')
+        s:create_index('primary')
+    end)
+end)
+
+g.test_mvcc_isolation_level_basics = function()
+    g.server:exec(function()
+        local t = require('luatest')
+        local fiber = require('fiber')
+        local s = box.space.test
+
+        local f = fiber.create(function()
+            fiber.self():set_joinable(true)
+            s:insert{1}
+        end)
+
+        t.assert_equals(s:select(), {})
+        t.assert_equals(s:count(), 0)
+
+        box.begin()
+        local res1 = s:select()
+        local res2 = s:count()
+        box.commit()
+        t.assert_equals(res1, {})
+        t.assert_equals(res2, 0)
+
+        local expect0 = {'default', 'read-confirmed', 'best-effort',
+                         box.txn_isolation_level.DEFAULT,
+                         box.txn_isolation_level.READ_CONFIRMED,
+                         box.txn_isolation_level.BEST_EFFORT,
+                         box.txn_isolation_level['default'],
+                         box.txn_isolation_level['read-confirmed'],
+                         box.txn_isolation_level['best-effort']}
+
+        for _,level in pairs(expect0) do
+            box.begin{txn_isolation = level}
+            res1 = s:select()
+            res2 = s:count()
+            box.commit()
+            t.assert_equals(res1, {})
+            t.assert_equals(res2, 0)
+        end
+
+        local expect0 = {'read-confirmed', 'best-effort',
+                         box.txn_isolation_level.READ_CONFIRMED,
+                         box.txn_isolation_level.BEST_EFFORT,
+                         box.txn_isolation_level['read-confirmed'],
+                         box.txn_isolation_level['best-effort']}
+
+        for _,level in pairs(expect0) do
+            box.cfg{txn_isolation = level}
+            box.begin{}
+            res1 = s:select()
+            res2 = s:count()
+            box.commit()
+            t.assert_equals(res1, {})
+            t.assert_equals(res2, 0)
+            box.begin{txn_isolation = 'default'}
+            res1 = s:select()
+            res2 = s:count()
+            box.commit()
+            t.assert_equals(res1, {})
+            t.assert_equals(res2, 0)
+            box.cfg{txn_isolation = 'best-effort'}
+        end
+
+        local expect1 = {'read-committed',
+                         box.txn_isolation_level.READ_COMMITTED,
+                         box.txn_isolation_level['read-committed']}
+
+        for _,level in pairs(expect1) do
+            box.begin{txn_isolation = level}
+            res1 = s:select()
+            res2 = s:count()
+            box.commit()
+            t.assert_equals(res1, {{1}})
+            t.assert_equals(res2, 1)
+        end
+
+        for _,level in pairs(expect1) do
+            box.cfg{txn_isolation = level}
+            box.begin{txn_isolation = level}
+            res1 = s:select()
+            res2 = s:count()
+            box.commit()
+            t.assert_equals(res1, {{1}})
+            t.assert_equals(res2, 1)
+            -- txn_isolation does not affect autocommit select,
+            -- which is always run as read-confirmed
+            t.assert_equals(s:select(), {})
+            t.assert_equals(s:count(), 0)
+            box.cfg{txn_isolation = 'best-effort'}
+        end
+
+        -- With default best-effort isolation RO->RW transaction can be aborted:
+        box.begin()
+        res1 = s:select(1) -- read confirmed {}
+        s:replace{2}
+        t.assert_error_msg_content_equals(
+            "Transaction has been aborted by conflict",
+            function() box.commit() end)
+        t.assert_equals(res1, {})
+
+        -- But using 'read-committed' allows to avoid conflict:
+        box.begin{txn_isolation = 'read-committed'}
+        res1 = s:select(1) -- read confirmed {{1}}
+        s:replace{2}
+        box.commit()
+        t.assert_equals(res1, {{1}})
+        t.assert_equals(s:select{}, {{1}, {2}})
+
+        f:join()
+    end)
+end
+
+g.after_test('test_mvcc_isolation_level_basics', function()
+    g.server:exec(function()
+        local s = box.space.test
+        if s then
+            s:drop()
+        end
+    end)
+end)
diff --git a/test/box/admin.result b/test/box/admin.result
index f5d6e6e7b6..7e47dc1cb6 100644
--- a/test/box/admin.result
+++ b/test/box/admin.result
@@ -113,6 +113,8 @@ cfg_filter(box.cfg)
     - true
   - - too_long_threshold
     - 0.5
+  - - txn_isolation
+    - best-effort
   - - txn_timeout
     - 3153600000
   - - vinyl_bloom_fpr
diff --git a/test/box/cfg.result b/test/box/cfg.result
index 8e953a0450..157fe1d976 100644
--- a/test/box/cfg.result
+++ b/test/box/cfg.result
@@ -101,6 +101,8 @@ cfg_filter(box.cfg)
  |     - true
  |   - - too_long_threshold
  |     - 0.5
+ |   - - txn_isolation
+ |     - best-effort
  |   - - txn_timeout
  |     - 3153600000
  |   - - vinyl_bloom_fpr
@@ -234,6 +236,8 @@ cfg_filter(box.cfg)
  |     - true
  |   - - too_long_threshold
  |     - 0.5
+ |   - - txn_isolation
+ |     - best-effort
  |   - - txn_timeout
  |     - 3153600000
  |   - - vinyl_bloom_fpr
diff --git a/test/box/misc.result b/test/box/misc.result
index cfb0151b63..776b14b2f7 100644
--- a/test/box/misc.result
+++ b/test/box/misc.result
@@ -105,6 +105,7 @@ t
   - stat
   - tuple
   - txn_id
+  - txn_isolation_level
   - unprepare
   - watch
 ...
-- 
GitLab