diff --git a/extra/exports b/extra/exports index efbe17eef29ca2c76c9d85cdfdd2dec9dd05a008..7456b584cd4b7f6c78378a5bbf595cad548c0977 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 675dc601048914ecc1daadaa012a0cdfe0c9bd0d..814b1779ca3565ef586511b8a4f285b061b95f32 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 2338e6bf24f55309a32b7fad179ade17c310130c..b53a0aad977beb9b54a9648dfd67a9fb1d395f20 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 5ea23917d3b4fed086e0d30ed3c354d72fc4538e..b3f2f333c7c40ec16047d203ce0c731d88d609f6 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 7bc2f83bf3f589d8b84a6f1db2f68ca6304cb83c..7d86dd1b2813f323d96b3073f205dd6a4b1cb820 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 154e98ed71eb52660395d4bb9bb4f160c5b37302..a03515524324e9b764598c7c1cb0eff3de5a2388 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 c8a36fc4c3be59e3ac676f8a466652781bf28d89..3b490790fd4779e88db1f66c5bde732428db2bad 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 fa9cab707ee1315d2df803226e92dc1c4dd6facb..af009c90b1937d279786f95729468cb1fcfa3231 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 de57b51d686b0bbf78cb72c678de4013539bedb2..491387d02ad754b8d4bfc032f5c4c928c3d316eb 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 b7e2ce09cb4505a6414171b7ccc62002552cdb3b..5fae022ea1c3a08b0793c5ba5d6a76e14db64d40 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 0000000000000000000000000000000000000000..94b670e95e937886878f1501194cf330e579493a --- /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 f5d6e6e7b6f3af7aef0d2ebf8e9546beee4bb3c1..7e47dc1cb6e8651ef219156acaa89e969758d144 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 8e953a04509c28da858394729b18b74fbc0b754f..157fe1d976630842b174dbb3ffda18ee1b204ebd 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 cfb0151b638388a5438c303a66bca379d6ccfad5..776b14b2f7064f86a88d5a19ea65648f529246a0 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 ...