From 552f7bddd65f8d4047646ad9b891cdfd7c5edf60 Mon Sep 17 00:00:00 2001 From: z00347042 Date: Thu, 11 Jun 2020 15:51:20 +0800 Subject: [PATCH] =?UTF-8?q?ARM=20TRX=5FSYS=20Hash=20Table=E5=8E=BB?= =?UTF-8?q?=E9=94=81=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CMakeLists.txt | 1 + configure.cmake | 1 - include/lf.h | 7 + mysql-test/include/have_lock_free_readview.inc | 6 + mysql-test/include/no_lock_free_readview.inc | 6 + ...innodb_i_s_innodb_trx_lock_free_readview.result | 81 ++ .../suite/innodb/t/innodb_i_s_innodb_trx.test | 2 + .../innodb_i_s_innodb_trx_lock_free_readview.test | 103 +++ mysys/lf_hash.cc | 161 ++++ storage/innobase/api/api0api.cc | 8 + storage/innobase/buf/buf0buf.cc | 21 + storage/innobase/clone/clone0copy.cc | 8 + storage/innobase/clone/clone0repl.cc | 2 + storage/innobase/dict/dict0dd.cc | 6 + storage/innobase/dict/dict0mem.cc | 5 + storage/innobase/handler/ha_innodb.cc | 56 +- storage/innobase/handler/ha_innopart.cc | 8 + storage/innobase/handler/handler0alter.cc | 12 + storage/innobase/handler/p_s.cc | 142 ++++ storage/innobase/include/clone0repl.h | 4 + storage/innobase/include/read0read.h | 3 + storage/innobase/include/read0types.h | 92 +++ storage/innobase/include/row0vers.h | 6 + storage/innobase/include/srv0srv.h | 3 + storage/innobase/include/sync0sync.h | 3 + storage/innobase/include/sync0types.h | 6 + storage/innobase/include/trx0roll.h | 10 + storage/innobase/include/trx0sys.h | 518 +++++++++++++ storage/innobase/include/trx0sys.ic | 2 + storage/innobase/include/trx0trx.h | 199 ++++- storage/innobase/include/trx0trx.ic | 20 + storage/innobase/include/trx0types.h | 2 + storage/innobase/include/trx0undo.h | 5 + storage/innobase/include/ut0counter.h | 2 +- storage/innobase/include/ut0new.h | 2 + storage/innobase/lock/lock0lock.cc | 471 ++++++++++++ storage/innobase/page/page0page.cc | 4 + storage/innobase/read/read0read.cc | 132 ++++ storage/innobase/row/row0ins.cc | 4 + storage/innobase/row/row0merge.cc | 19 + storage/innobase/row/row0mysql.cc | 4 + storage/innobase/row/row0pread.cc | 7 + storage/innobase/row/row0row.cc | 7 + storage/innobase/row/row0sel.cc | 66 +- storage/innobase/row/row0upd.cc | 6 + storage/innobase/row/row0vers.cc | 61 +- storage/innobase/srv/srv0srv.cc | 17 + storage/innobase/srv/srv0start.cc | 36 + storage/innobase/sync/sync0debug.cc | 14 + storage/innobase/sync/sync0sync.cc | 3 + storage/innobase/trx/trx0i_s.cc | 44 ++ storage/innobase/trx/trx0purge.cc | 20 + storage/innobase/trx/trx0roll.cc | 104 +++ storage/innobase/trx/trx0sys.cc | 581 ++++++++++++++- storage/innobase/trx/trx0trx.cc | 827 ++++++++++++++++++++- storage/innobase/trx/trx0undo.cc | 66 ++ storage/innobase/ut/ut0new.cc | 4 + unittest/gunit/mysys_lf-t.cc | 17 + 58 files changed, 4011 insertions(+), 16 deletions(-) create mode 100644 mysql-test/include/have_lock_free_readview.inc create mode 100644 mysql-test/include/no_lock_free_readview.inc create mode 100644 mysql-test/suite/innodb/r/innodb_i_s_innodb_trx_lock_free_readview.result create mode 100644 mysql-test/suite/innodb/t/innodb_i_s_innodb_trx_lock_free_readview.test diff --git a/CMakeLists.txt b/CMakeLists.txt index ce1d1bb..b450a81 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1031,6 +1031,7 @@ ADD_DEFINITIONS(-D__STDC_LIMIT_MACROS) # Enable C99 limit macros ADD_DEFINITIONS(-D__STDC_FORMAT_MACROS) # Enable C99 printf format macros ADD_DEFINITIONS(-D_USE_MATH_DEFINES) # Get access to M_PI, M_E, etc. in math.h ADD_DEFINITIONS(-DLZ4_DISABLE_DEPRECATE_WARNINGS) # C++14 deprecation warnings in LZ4. +ADD_DEFINITIONS(-DHAVE_LOCK_FREE_READVIEW) # Enable lock free readview feature OPTION(ENABLE_EXPERIMENT_SYSVARS "Expose ussually hidden system variables to allow experiments" OFF) IF(ENABLE_EXPERIMENT_SYSVARS) diff --git a/configure.cmake b/configure.cmake index dc54abf..ec61824 100644 --- a/configure.cmake +++ b/configure.cmake @@ -31,7 +31,6 @@ INCLUDE (CheckCSourceRuns) INCLUDE (CheckCXXSourceRuns) INCLUDE (CheckSymbolExists) - # System type affects version_compile_os variable IF(NOT SYSTEM_TYPE) IF(PLATFORM) diff --git a/include/lf.h b/include/lf.h index 53898f4..6e25ec6 100644 --- a/include/lf.h +++ b/include/lf.h @@ -159,6 +159,9 @@ struct LF_HASH; typedef uint lf_hash_func(const LF_HASH *, const uchar *, size_t); typedef void lf_hash_init_func(uchar *dst, const uchar *src); +#ifdef HAVE_LOCK_FREE_READVIEW +typedef bool lf_hash_walk_func(void *, void *); +#endif /* HAVE_LOCK_FREE_READVIEW */ #define LF_HASH_UNIQUE 1 #define MY_LF_ERRPTR ((void *)(intptr)1) @@ -214,6 +217,10 @@ int lf_hash_insert(LF_HASH *hash, LF_PINS *pins, const void *data); void *lf_hash_search(LF_HASH *hash, LF_PINS *pins, const void *key, uint keylen); int lf_hash_delete(LF_HASH *hash, LF_PINS *pins, const void *key, uint keylen); +#ifdef HAVE_LOCK_FREE_READVIEW +int lf_hash_iterate(LF_HASH *hash, LF_PINS *pins, const lf_hash_walk_func *callback, + const void *argument); +#endif /* HAVE_LOCK_FREE_READVIEW */ static inline LF_PINS *lf_hash_get_pins(LF_HASH *hash) { return lf_pinbox_get_pins(&hash->alloc.pinbox); diff --git a/mysql-test/include/have_lock_free_readview.inc b/mysql-test/include/have_lock_free_readview.inc new file mode 100644 index 0000000..a24932e --- /dev/null +++ b/mysql-test/include/have_lock_free_readview.inc @@ -0,0 +1,6 @@ +--let $_have_lock_free_readview = `select VARIABLE_VALUE from performance_schema.global_status where VARIABLE_NAME = 'Innodb_have_lock_free_readview';` + +if ($_have_lock_free_readview == "") +{ + --skip Need lock free readview binaries +} diff --git a/mysql-test/include/no_lock_free_readview.inc b/mysql-test/include/no_lock_free_readview.inc new file mode 100644 index 0000000..779d9eb --- /dev/null +++ b/mysql-test/include/no_lock_free_readview.inc @@ -0,0 +1,6 @@ +--let $_have_lock_free_readview = `select VARIABLE_VALUE from performance_schema.global_status where VARIABLE_NAME = 'Innodb_have_lock_free_readview';` + +if ($_have_lock_free_readview == "ON") +{ + --skip Doesn't support lock free readview binaries +} diff --git a/mysql-test/suite/innodb/r/innodb_i_s_innodb_trx_lock_free_readview.result b/mysql-test/suite/innodb/r/innodb_i_s_innodb_trx_lock_free_readview.result new file mode 100644 index 0000000..73e742c --- /dev/null +++ b/mysql-test/suite/innodb/r/innodb_i_s_innodb_trx_lock_free_readview.result @@ -0,0 +1,81 @@ +SET GLOBAL innodb_lock_wait_timeout=600; +DESCRIBE INFORMATION_SCHEMA.INNODB_TRX; +Field Type Null Key Default Extra +trx_id varchar(18) NO +trx_state varchar(13) NO +trx_started datetime NO +trx_requested_lock_id varchar(105) YES +trx_wait_started datetime YES +trx_weight bigint unsigned NO +trx_mysql_thread_id bigint unsigned NO +trx_query varchar(1024) YES +trx_operation_state varchar(64) YES +trx_tables_in_use bigint unsigned NO +trx_tables_locked bigint unsigned NO +trx_lock_structs bigint unsigned NO +trx_lock_memory_bytes bigint unsigned NO +trx_rows_locked bigint unsigned NO +trx_rows_modified bigint unsigned NO +trx_concurrency_tickets bigint unsigned NO +trx_isolation_level varchar(16) NO +trx_unique_checks int NO +trx_foreign_key_checks int NO +trx_last_foreign_key_error varchar(256) YES +trx_adaptive_hash_latched int NO +trx_adaptive_hash_timeout bigint unsigned NO +trx_is_read_only int NO +trx_autocommit_non_locking int NO +DROP TABLE IF EXISTS t1; +CREATE TABLE t1 ( +c01 INT, +c02 INT, +PRIMARY KEY (c01) +) ENGINE=INNODB STATS_AUTO_RECALC=0; +INSERT INTO t1 VALUES +(1,2),(2,4),(3,6),(4,8); +CREATE TABLE t2 ( +c01 INT, +c02 INT, +PRIMARY KEY (c01), +CONSTRAINT fk1 FOREIGN KEY (c02) REFERENCES t1 (c01) +) ENGINE=INNODB STATS_AUTO_RECALC=0; +INSERT INTO t2 VALUES +(1,1),(2,2),(3,3); +SET autocommit=0; +INSERT INTO t1 VALUES (5,10); +SELECT * FROM t1 FOR UPDATE; +c01 c02 +1 2 +2 4 +3 6 +4 8 +5 10 +SELECT trx_state, trx_weight, trx_tables_in_use, trx_tables_locked, +trx_rows_locked, trx_rows_modified, trx_concurrency_tickets, +trx_isolation_level, trx_unique_checks, trx_foreign_key_checks +FROM INFORMATION_SCHEMA.INNODB_TRX; +trx_state trx_weight trx_tables_in_use trx_tables_locked trx_rows_locked trx_rows_modified trx_concurrency_tickets trx_isolation_level trx_unique_checks trx_foreign_key_checks +RUNNING 3 0 1 5 1 0 REPEATABLE READ 1 1 +ROLLBACK; +SET FOREIGN_KEY_CHECKS = 0; +SET UNIQUE_CHECKS = 0; +SET TRANSACTION ISOLATION LEVEL SERIALIZABLE; +BEGIN; +INSERT INTO t1 VALUES (6,12); +SELECT trx_isolation_level, trx_unique_checks, trx_foreign_key_checks +FROM INFORMATION_SCHEMA.INNODB_TRX; +trx_isolation_level trx_unique_checks trx_foreign_key_checks +SERIALIZABLE 0 0 +ROLLBACK; +SET FOREIGN_KEY_CHECKS = 1; +SET UNIQUE_CHECKS = 1; +BEGIN; +INSERT INTO t2 VALUES (4,10); +ERROR 23000: Cannot add or update a child row: a foreign key constraint fails (`test`.`t2`, CONSTRAINT `fk1` FOREIGN KEY (`c02`) REFERENCES `t1` (`c01`)) +SELECT trx_state, trx_isolation_level, trx_last_foreign_key_error +FROM INFORMATION_SCHEMA.INNODB_TRX; +trx_state trx_isolation_level trx_last_foreign_key_error +RUNNING REPEATABLE READ `test`.`t2`, CONSTRAINT `fk1` FOREIGN KEY (`c02`) REFERENCES `t1` (`c01`) +DROP TABLE t2; +DROP TABLE t1; +SET GLOBAL innodb_lock_wait_timeout=default; diff --git a/mysql-test/suite/innodb/t/innodb_i_s_innodb_trx.test b/mysql-test/suite/innodb/t/innodb_i_s_innodb_trx.test index b92cc8f..1fbc71f 100644 --- a/mysql-test/suite/innodb/t/innodb_i_s_innodb_trx.test +++ b/mysql-test/suite/innodb/t/innodb_i_s_innodb_trx.test @@ -6,6 +6,8 @@ # Make sure the locks keep waiting until they are released, # even on a busy system. +--source include/no_lock_free_readview.inc + SET GLOBAL innodb_lock_wait_timeout=600; DESCRIBE INFORMATION_SCHEMA.INNODB_TRX; diff --git a/mysql-test/suite/innodb/t/innodb_i_s_innodb_trx_lock_free_readview.test b/mysql-test/suite/innodb/t/innodb_i_s_innodb_trx_lock_free_readview.test new file mode 100644 index 0000000..7cc3709 --- /dev/null +++ b/mysql-test/suite/innodb/t/innodb_i_s_innodb_trx_lock_free_readview.test @@ -0,0 +1,103 @@ + +# +# Test that transaction data is correctly "visualized" in +# INFORMATION_SCHEMA.INNODB_TRX +# + +# Make sure the locks keep waiting until they are released, +# even on a busy system. +--source include/have_lock_free_readview.inc + +SET GLOBAL innodb_lock_wait_timeout=600; + +DESCRIBE INFORMATION_SCHEMA.INNODB_TRX; + +-- disable_warnings +DROP TABLE IF EXISTS t1; +-- enable_warnings + +CREATE TABLE t1 ( + c01 INT, + c02 INT, + PRIMARY KEY (c01) +) ENGINE=INNODB STATS_AUTO_RECALC=0; + +INSERT INTO t1 VALUES +(1,2),(2,4),(3,6),(4,8); + +CREATE TABLE t2 ( + c01 INT, + c02 INT, + PRIMARY KEY (c01), + CONSTRAINT fk1 FOREIGN KEY (c02) REFERENCES t1 (c01) +) ENGINE=INNODB STATS_AUTO_RECALC=0; + +INSERT INTO t2 VALUES +(1,1),(2,2),(3,3); + +-- source include/count_sessions.inc + +-- connect (con_trx,localhost,root,,) +-- connect (con_verify_innodb_trx,localhost,root,,) + +-- connection con_trx +SET autocommit=0; +INSERT INTO t1 VALUES (5,10); +SELECT * FROM t1 FOR UPDATE; + +let $wait_timeout= 300; +let $wait_condition= + SELECT COUNT(*) = 1 FROM INFORMATION_SCHEMA.INNODB_TRX; +-- source include/wait_condition.inc + +-- connection con_verify_innodb_trx +SELECT trx_state, trx_weight, trx_tables_in_use, trx_tables_locked, +trx_rows_locked, trx_rows_modified, trx_concurrency_tickets, +trx_isolation_level, trx_unique_checks, trx_foreign_key_checks +FROM INFORMATION_SCHEMA.INNODB_TRX; + +-- connection con_trx +ROLLBACK; +SET FOREIGN_KEY_CHECKS = 0; +SET UNIQUE_CHECKS = 0; +SET TRANSACTION ISOLATION LEVEL SERIALIZABLE; +BEGIN; +INSERT INTO t1 VALUES (6,12); + +let $wait_timeout= 300; +let $wait_condition= + SELECT trx_unique_checks = 0 FROM INFORMATION_SCHEMA.INNODB_TRX; +-- source include/wait_condition.inc + +-- connection con_verify_innodb_trx +SELECT trx_isolation_level, trx_unique_checks, trx_foreign_key_checks +FROM INFORMATION_SCHEMA.INNODB_TRX; + +-- connection con_trx +ROLLBACK; +SET FOREIGN_KEY_CHECKS = 1; +SET UNIQUE_CHECKS = 1; +BEGIN; +-- error 1452 +INSERT INTO t2 VALUES (4,10); + +let $wait_timeout= 300; +let $wait_condition= + SELECT trx_unique_checks = 1 FROM INFORMATION_SCHEMA.INNODB_TRX; +-- source include/wait_condition.inc + +-- connection con_verify_innodb_trx +SELECT trx_state, trx_isolation_level, trx_last_foreign_key_error +FROM INFORMATION_SCHEMA.INNODB_TRX; + +-- connection default + +-- disconnect con_trx +-- disconnect con_verify_innodb_trx + +DROP TABLE t2; +DROP TABLE t1; + +-- source include/wait_until_count_sessions.inc + +SET GLOBAL innodb_lock_wait_timeout=default; diff --git a/mysys/lf_hash.cc b/mysys/lf_hash.cc index cf0cffa..e6f8ec7 100644 --- a/mysys/lf_hash.cc +++ b/mysys/lf_hash.cc @@ -101,6 +101,126 @@ static inline T *SET_DELETED(T *ptr) { return reinterpret_cast(i); } +#ifdef HAVE_LOCK_FREE_READVIEW +/** + Walk the list, searching for an element or invoking a callback. + + Search for hashnr/key/keylen in the list starting from 'head' and position the + cursor. The list is ORDER by hashnr, key + + @param head start walking the list from this node + @param cs charset for comparing keys, nullptr if callback is used + @param hashnr hash number to searching for + @param key key to search for OR data for the callback + @param keylen length of the key to compare, 0 if callback is used + @param cursor for returning the found element + @param pins see lf_alloc-pin.cc + @param callback callback action, invoked for every element + + @note + cursor is positioned in either case + pins[0..2] are used, they are not removed on return + callback might see some elements twice (because of retries) + + @return + if find: 0 - not found + 1 - found + if callback: + 0 - ok + 1 - error (callback returned true) +*/ +static int my_lfind(std::atomic *head, CHARSET_INFO *cs, + uint32 hashnr, const uchar *key, size_t keylen, + CURSOR *cursor, LF_PINS *pins, + lf_hash_walk_func *callback) { + uint32 cur_hashnr; + const uchar *cur_key; + size_t cur_keylen; + LF_SLIST *link; + + /* should not be set both */ + DBUG_ASSERT((cs == nullptr) || (callback == nullptr)); + /* should not be set both */ + DBUG_ASSERT((keylen == 0) || (callback == nullptr)); + +retry: + cursor->prev = head; + do /* PTR() isn't necessary below, head is a dummy node */ + { + cursor->curr = (LF_SLIST *)(*cursor->prev); + lf_pin(pins, 1, cursor->curr); + } while (*cursor->prev != cursor->curr && LF_BACKOFF); + for (;;) { + + if (unlikely(!cursor->curr)) { + return 0; /* end of the list */ + } + + cur_hashnr = cursor->curr->hashnr; + cur_keylen = cursor->curr->keylen; + cur_key = cursor->curr->key; + + do { + link = cursor->curr->link; + cursor->next = PTR(link); + lf_pin(pins, 0, cursor->next); + } while (link != cursor->curr->link && LF_BACKOFF); + + if (!DELETED(link)) { + if (likely(callback != nullptr)) { + if ((cur_hashnr & 1) > 0 && + callback(cursor->curr + 1, + const_cast(static_cast(key)))) { + return 1; + } + } else if (cur_hashnr >= hashnr) { + int r = 1; + if (cur_hashnr > hashnr || + (r = my_strnncoll(cs, cur_key, cur_keylen, key, keylen)) >= 0) { + return !r; + } + } + cursor->prev = &(cursor->curr->link); + if (!(cur_hashnr & 1)) /* dummy node */ + head = cursor->prev; + lf_pin(pins, 2, cursor->curr); + } else { + /* + we found a deleted node - be nice, help the other thread + and remove this deleted node + */ + if (atomic_compare_exchange_strong(cursor->prev, &cursor->curr, + cursor->next)) { + lf_pinbox_free(pins, cursor->curr); + } else { + (void)LF_BACKOFF; + goto retry; + } + } + cursor->curr = cursor->next; + lf_pin(pins, 1, cursor->curr); + } +} + +/* + DESCRIPTION + Search for hashnr/key/keylen in the list starting from 'head' and + position the cursor. The list is ORDER BY hashnr, key + + RETURN + 0 - not found + 1 - found + + NOTE + cursor is positioned in either case + pins[0..2] are used, they are NOT removed on return +*/ +static int my_lfind(std::atomic *head, CHARSET_INFO *cs, + uint32 hashnr, const uchar *key, size_t keylen, + CURSOR *cursor, LF_PINS *pins) { + return my_lfind(head, cs, hashnr, key, keylen, cursor, pins, nullptr); +} +#else /* DESCRIPTION Search for hashnr/key/keylen in the list starting from 'head' and @@ -173,6 +293,7 @@ retry: lf_pin(pins, 1, cursor->curr); } } +#endif /* HAVE_LOCK_FREE_READVIEW */ /** Search for list element satisfying condition specified by match @@ -570,6 +691,46 @@ int lf_hash_delete(LF_HASH *hash, LF_PINS *pins, const void *key, uint keylen) { return 0; } +#ifdef HAVE_LOCK_FREE_READVIEW +/** + Iterate over all elements in hash and call function with the element. + + @note + If one of 'callback' invocations returns true the iteration aborts. + 'action' might see some elements twice! + + @return 0 if ok, or 1 if error (action returned true) +*/ +int lf_hash_iterate(LF_HASH *hash, LF_PINS *pins, const lf_hash_walk_func *callback, + const void *argument) { + CURSOR cursor; + uint bucket = 0; + int res; + std::atomic *el; + + el = static_cast *>( + lf_dynarray_lvalue(&hash->array, bucket)); + if (unlikely(el == nullptr)) { + /* if there's no bucket==0, the hash is empty */ + return 0; + } + + if (*el == nullptr && unlikely(initialize_bucket(hash, el, bucket, pins))) { + /* if there's no bucket==0, the hash is empty */ + return 0; + } + + res = my_lfind(el, nullptr, 0, static_cast(argument), 0, &cursor, + pins, callback); + + lf_unpin(pins, 2); + lf_unpin(pins, 1); + lf_unpin(pins, 0); + + return res; +} +#endif /* HAVE_LOCK_FREE_READVIEW */ + /** Find hash element corresponding to the key. diff --git a/storage/innobase/api/api0api.cc b/storage/innobase/api/api0api.cc index 044ee0b..51c0a41 100644 --- a/storage/innobase/api/api0api.cc +++ b/storage/innobase/api/api0api.cc @@ -743,7 +743,11 @@ static ib_err_t ib_create_cursor(ib_crsr_t *ib_crsr, /*!< out: InnoDB cursor */ /* Assign a read view if the transaction does not have it yet */ +#ifdef HAVE_LOCK_FREE_READVIEW + prebuilt->trx->read_view.open(prebuilt->trx); +#else trx_assign_read_view(prebuilt->trx); +#endif /* HAVE_LOCK_FREE_READVIEW */ } *ib_crsr = (ib_crsr_t)cursor; @@ -934,7 +938,11 @@ ib_err_t ib_cursor_new_trx(ib_crsr_t ib_crsr, /*!< in/out: InnoDB cursor */ cursor->valid_trx = TRUE; +#ifdef HAVE_LOCK_FREE_READVIEW + prebuilt->trx->read_view.open(prebuilt->trx); +#else trx_assign_read_view(prebuilt->trx); +#endif /* HAVE_LOCK_FREE_READVIEW */ ib_qry_proc_free(&cursor->q_proc); diff --git a/storage/innobase/buf/buf0buf.cc b/storage/innobase/buf/buf0buf.cc index 983603e..f3191db 100644 --- a/storage/innobase/buf/buf0buf.cc +++ b/storage/innobase/buf/buf0buf.cc @@ -2143,6 +2143,26 @@ withdraw_retry: lock_mutex_enter(); trx_sys_mutex_enter(); +#ifdef HAVE_LOCK_FREE_READVIEW + bool found = false; + for (trx_t *trx = UT_LIST_GET_FIRST(trx_sys->trx_list); trx != nullptr; + trx = UT_LIST_GET_NEXT(trx_list, trx)) { + if (trx->state != TRX_STATE_NOT_STARTED && trx->mysql_thd != nullptr && + ut_difftime(withdraw_started, trx->start_time) > 0) { + if (!found) { + ib::warn(ER_IB_MSG_61) << "The following trx might hold" + " the blocks in buffer pool to" + " be withdrawn. Buffer pool" + " resizing can complete only" + " after all the transactions" + " below release the blocks."; + found = true; + } + + lock_trx_print_wait_and_mvcc_state(stderr, trx); + } + } +#else bool found = false; for (trx_t *trx = UT_LIST_GET_FIRST(trx_sys->mysql_trx_list); trx != NULL; trx = UT_LIST_GET_NEXT(mysql_trx_list, trx)) { @@ -2161,6 +2181,7 @@ withdraw_retry: lock_trx_print_wait_and_mvcc_state(stderr, trx); } } +#endif /* HAVE_LOCK_FREE_READVIEW */ trx_sys_mutex_exit(); lock_mutex_exit(); diff --git a/storage/innobase/clone/clone0copy.cc b/storage/innobase/clone/clone0copy.cc index b18c8b4..4dc0092 100644 --- a/storage/innobase/clone/clone0copy.cc +++ b/storage/innobase/clone/clone0copy.cc @@ -327,14 +327,22 @@ int Clone_Snapshot::update_binlog_position() { } int Clone_Snapshot::wait_trx_end(THD *thd, trx_id_t trx_id) { +#ifdef HAVE_LOCK_FREE_READVIEW + auto trx = trx_sys->find(current_trx(), trx_id, false); +#else auto trx = trx_rw_is_active(trx_id, nullptr, false); +#endif if (trx == nullptr) { return (0); } auto wait_cond = [&](bool alert, bool &result) { /* Check if transaction is still active. */ +#ifdef HAVE_LOCK_FREE_READVIEW + auto trx = trx_sys->find(current_trx(), trx_id, false); +#else auto trx = trx_rw_is_active(trx_id, nullptr, false); +#endif if (trx == nullptr) { result = false; return (0); diff --git a/storage/innobase/clone/clone0repl.cc b/storage/innobase/clone/clone0repl.cc index 8b9484c..61928e5 100644 --- a/storage/innobase/clone/clone0repl.cc +++ b/storage/innobase/clone/clone0repl.cc @@ -49,7 +49,9 @@ void Clone_persist_gtid::add(const Gtid_desc >id_desc) { if (!is_active() || gtid_table_persistor == nullptr) { return; } +#ifndef HAVE_LOCK_FREE_READVIEW ut_ad(trx_sys_mutex_own()); +#endif /* Get active GTID list */ auto ¤t_gtids = get_active_list(); diff --git a/storage/innobase/dict/dict0dd.cc b/storage/innobase/dict/dict0dd.cc index 01631fd..df40c27 100644 --- a/storage/innobase/dict/dict0dd.cc +++ b/storage/innobase/dict/dict0dd.cc @@ -6009,9 +6009,15 @@ the dictionary. bool dd_tablespace_update_cache(THD *thd) { /* If there are no prepared trxs, then DD reads would have been already consistent. No need to update cache */ +#ifdef HAVE_LOCK_FREE_READVIEW + if (!trx_sys->found_prepared_trx()) { + return (false); + } +#else if (!trx_sys->found_prepared_trx) { return (false); } +#endif /* HAVE_LOCK_FREE_READVIEW */ dd::cache::Dictionary_client *dc = dd::get_dd_client(thd); dd::cache::Dictionary_client::Auto_releaser releaser(dc); diff --git a/storage/innobase/dict/dict0mem.cc b/storage/innobase/dict/dict0mem.cc index 02bccdc..8e715e2 100644 --- a/storage/innobase/dict/dict0mem.cc +++ b/storage/innobase/dict/dict0mem.cc @@ -596,9 +596,14 @@ bool dict_index_t::is_usable(const trx_t *trx) const { } /* Check if the specified transaction can see this index. */ +#ifdef HAVE_LOCK_FREE_READVIEW + return (table->is_temporary() || trx_id == 0 || !trx->read_view.is_open() || + trx->read_view.changes_visible(trx_id, table->name)); +#else return (table->is_temporary() || trx_id == 0 || !MVCC::is_view_active(trx->read_view) || trx->read_view->changes_visible(trx_id, table->name)); +#endif /* HAVE_LOCK_FREE_READVIEW */ } #endif /* !UNIV_HOTBACKUP */ diff --git a/storage/innobase/handler/ha_innodb.cc b/storage/innobase/handler/ha_innodb.cc index 75c2b84..9592b5a 100644 --- a/storage/innobase/handler/ha_innodb.cc +++ b/storage/innobase/handler/ha_innodb.cc @@ -671,6 +671,9 @@ static PSI_mutex_info all_innodb_mutexes[] = { PSI_MUTEX_KEY(rtr_match_mutex, 0, 0, PSI_DOCUMENT_ME), PSI_MUTEX_KEY(rtr_path_mutex, 0, 0, PSI_DOCUMENT_ME), PSI_MUTEX_KEY(rtr_ssn_mutex, 0, 0, PSI_DOCUMENT_ME), +#ifdef HAVE_LOCK_FREE_READVIEW + PSI_MUTEX_KEY(rw_trx_hash_element_mutex, 0, 0, PSI_DOCUMENT_ME), +#endif /* HAVE_LOCK_FREE_READVIEW */ PSI_MUTEX_KEY(trx_sys_mutex, 0, 0, PSI_DOCUMENT_ME), PSI_MUTEX_KEY(zip_pad_mutex, 0, 0, PSI_DOCUMENT_ME), PSI_MUTEX_KEY(master_key_id_mutex, 0, 0, PSI_DOCUMENT_ME), @@ -1059,6 +1062,11 @@ static SHOW_VAR innodb_status_variables[] = { {"undo_tablespaces_active", (char *)&export_vars.innodb_undo_tablespaces_active, SHOW_LONG, SHOW_SCOPE_GLOBAL}, +#ifdef HAVE_LOCK_FREE_READVIEW + {"have_lock_free_readview", + (char *)&export_vars.innodb_have_lock_free_readview, SHOW_BOOL, + SHOW_SCOPE_GLOBAL}, +#endif #ifdef UNIV_DEBUG {"purge_trx_id_age", (char *)&export_vars.innodb_purge_trx_id_age, SHOW_LONG, SHOW_SCOPE_GLOBAL}, @@ -2865,6 +2873,20 @@ ibool trx_is_strict(trx_t *trx) /*!< in: transaction */ (!trx->in_truncate)); } +#ifdef HAVE_LOCK_FREE_READVIEW +/** Gets current trx. + + This function may be called during InnoDB initialisation, when + innodb_hton_ptr->slot is not yet set to meaningful value. */ +trx_t *current_trx() { + THD *thd = current_thd; + if (UNIV_LIKELY(thd != nullptr) && innodb_hton_ptr->slot != HA_SLOT_UNDEF) { + return thd_to_trx(thd); + } + return nullptr; +} +#endif /* HAVE_LOCK_FREE_READVIEW */ + /** Resets some fields of a m_prebuilt struct. The template is used in fast retrieval of just those column values MySQL needs in its processing. */ void ha_innobase::reset_template(void) { @@ -2918,7 +2940,11 @@ void ha_innobase::init_table_handle_for_HANDLER(void) { /* Assign a read view if the transaction does not have it yet */ +#ifdef HAVE_LOCK_FREE_READVIEW + m_prebuilt->trx->read_view.open(m_prebuilt->trx); +#else trx_assign_read_view(m_prebuilt->trx); +#endif /* HAVE_LOCK_FREE_READVIEW */ innobase_register_trx(ht, m_user_thd, m_prebuilt->trx); @@ -4939,10 +4965,17 @@ static int innobase_init_files(dict_init_mode_t dict_init_mode, return innodb_init_abort(); } +#ifdef HAVE_LOCK_FREE_READVIEW + if (trx_sys->found_prepared_trx() > 0) { + ib::error(ER_DD_UPGRADE_FOUND_PREPARED_XA_TRANSACTION); + return innodb_init_abort(); + } +#else if (trx_sys->found_prepared_trx > 0) { ib::error(ER_DD_UPGRADE_FOUND_PREPARED_XA_TRANSACTION); return innodb_init_abort(); } +#endif /* Disable AHI when we start loading tables for purge. These tables are evicted anyway after purge. */ @@ -5185,7 +5218,11 @@ static int innobase_start_trx_and_assign_read_view( innobase_map_isolation_level(thd_get_trx_isolation(thd)); if (trx->isolation_level == TRX_ISO_REPEATABLE_READ) { +#ifdef HAVE_LOCK_FREE_READVIEW + trx->read_view.open(trx); +#else trx_assign_read_view(trx); +#endif /* HAVE_LOCK_FREE_READVIEW */ } else { push_warning_printf(thd, Sql_condition::SL_WARNING, HA_ERR_UNSUPPORTED, "InnoDB: WITH CONSISTENT SNAPSHOT" @@ -10091,7 +10128,11 @@ int ha_innobase::sample_init(void *&scan_ctx, double sampling_percentage, auto trx = m_prebuilt->trx; innobase_register_trx(ht, ha_thd(), trx); trx_start_if_not_started_xa(trx, false); +#ifdef HAVE_LOCK_FREE_READVIEW + trx->read_view.open(trx); +#else trx_assign_read_view(trx); +#endif /* Parallel read is not currently supported for sampling. */ size_t n_threads = 1; @@ -17774,7 +17815,10 @@ int ha_innobase::external_lock(THD *thd, /*!< in: handle to the user thread */ trx->is_dd_trx here */ ut_d(trx->is_dd_trx = false); } - +#ifdef HAVE_LOCK_FREE_READVIEW + } else if (trx->isolation_level <= TRX_ISO_READ_COMMITTED) { + trx->read_view.close(); +#else } else if (trx->isolation_level <= TRX_ISO_READ_COMMITTED && MVCC::is_view_active(trx->read_view)) { mutex_enter(&trx_sys->mutex); @@ -17782,6 +17826,7 @@ int ha_innobase::external_lock(THD *thd, /*!< in: handle to the user thread */ trx_sys->mvcc->view_close(trx->read_view, true); mutex_exit(&trx_sys->mutex); +#endif /* HAVE_LOCK_FREE_READVIEW */ } } @@ -18377,6 +18422,14 @@ THR_LOCK_DATA **ha_innobase::store_lock( trx->isolation_level = innobase_map_isolation_level((enum_tx_isolation)thd_tx_isolation(thd)); +#ifdef HAVE_LOCK_FREE_READVIEW + if (trx->isolation_level <= TRX_ISO_READ_COMMITTED) { + /* At low transaction isolation levels we let + each consistent read set its own snapshot */ + + trx->read_view.close(); + } +#else if (trx->isolation_level <= TRX_ISO_READ_COMMITTED && MVCC::is_view_active(trx->read_view)) { /* At low transaction isolation levels we let @@ -18388,6 +18441,7 @@ THR_LOCK_DATA **ha_innobase::store_lock( mutex_exit(&trx_sys->mutex); } +#endif /* HAVE_LOCK_FREE_READVIEW */ } DBUG_ASSERT(EQ_CURRENT_THD(thd)); diff --git a/storage/innobase/handler/ha_innopart.cc b/storage/innobase/handler/ha_innopart.cc index e684f4e..ffa06b7 100644 --- a/storage/innobase/handler/ha_innopart.cc +++ b/storage/innobase/handler/ha_innopart.cc @@ -2031,7 +2031,11 @@ int ha_innopart::sample_init(void *&scan_ctx, double sampling_percentage, auto trx = m_prebuilt->trx; innobase_register_trx(ht, ha_thd(), trx); trx_start_if_not_started_xa(trx, false); +#ifdef HAVE_LOCK_FREE_READVIEW + trx->read_view.open(trx); +#else trx_assign_read_view(trx); +#endif /* Parallel read is not currently supported for sampling. */ size_t n_threads = 1; @@ -3178,7 +3182,11 @@ int ha_innopart::records(ha_rows *num_rows) { trx->mysql_n_tables_locked == 0 && !m_prebuilt->ins_sel_stmt && n_threads > 1) { trx_start_if_not_started_xa(trx, false); +#ifdef HAVE_LOCK_FREE_READVIEW + trx->read_view.open(trx); +#else trx_assign_read_view(trx); +#endif const auto first_used_partition = m_part_info->get_first_used_partition(); diff --git a/storage/innobase/handler/handler0alter.cc b/storage/innobase/handler/handler0alter.cc index dfb5d0f..7bc1384 100644 --- a/storage/innobase/handler/handler0alter.cc +++ b/storage/innobase/handler/handler0alter.cc @@ -1173,7 +1173,11 @@ int ha_innobase::parallel_scan_init(void *&scan_ctx, size_t &num_threads) { trx_start_if_not_started_xa(trx, false); +#ifdef HAVE_LOCK_FREE_READVIEW + trx->read_view.open(trx); +#else trx_assign_read_view(trx); +#endif size_t n_threads = thd_parallel_read_threads(m_prebuilt->trx->mysql_thd); @@ -4778,7 +4782,11 @@ static MY_ATTRIBUTE((warn_unused_result)) bool prepare_inplace_alter_table_dict( if (ctx->online) { /* Assign a consistent read view for row_merge_read_clustered_index(). */ +#ifdef HAVE_LOCK_FREE_READVIEW + ctx->prebuilt->trx->read_view.open(ctx->prebuilt->trx); +#else trx_assign_read_view(ctx->prebuilt->trx); +#endif /* HAVE_LOCK_FREE_READVIEW */ } if (fts_index) { @@ -9750,7 +9758,11 @@ int ha_innopart::parallel_scan_init(void *&scan_ctx, size_t &num_threads) { trx_start_if_not_started_xa(trx, false); +#ifdef HAVE_LOCK_FREE_READVIEW + trx->read_view.open(trx); +#else trx_assign_read_view(trx); +#endif const Parallel_reader::Scan_range FULL_SCAN{}; diff --git a/storage/innobase/handler/p_s.cc b/storage/innobase/handler/p_s.cc index 74c9f94..b3fe28e 100644 --- a/storage/innobase/handler/p_s.cc +++ b/storage/innobase/handler/p_s.cc @@ -143,6 +143,11 @@ this program; if not, write to the Free Software Foundation, Inc., static const char *g_engine = "INNODB"; static const size_t g_engine_length = 6; +#ifdef HAVE_LOCK_FREE_READVIEW +inline trx_t *get_next_trx(const trx_t *trx) { + return (UT_LIST_GET_NEXT(trx_list, trx)); +} +#else inline trx_t *get_next_trx(const trx_t *trx, bool read_write) { if (read_write) { return (UT_LIST_GET_NEXT(trx_list, trx)); @@ -150,10 +155,17 @@ inline trx_t *get_next_trx(const trx_t *trx, bool read_write) { return (UT_LIST_GET_NEXT(mysql_trx_list, trx)); } } +#endif /* HAVE_LOCK_FREE_READVIEW */ /** Pass of a given scan. */ enum scan_pass { INIT_SCANNING, +#ifdef HAVE_LOCK_FREE_READVIEW + /** Scan the trx list. + @sa trx_sys_t::trx_list + */ + SCANNING_TRX_LIST, +#else /** Scan the RW trx list. @sa trx_sys_t::rw_trx_list */ @@ -162,6 +174,7 @@ enum scan_pass { @sa trx_t::mysql_trx_list */ SCANNING_MYSQL_TRX_LIST, +#endif /* HAVE_LOCK_FREE_READVIEW */ DONE_SCANNING }; @@ -202,6 +215,17 @@ class Innodb_trx_scan_state { m_next_trx_id_range = TRX_ID_MAX; } else { switch (m_scan_pass) { +#ifdef HAVE_LOCK_FREE_READVIEW + case INIT_SCANNING: + m_scan_pass = SCANNING_TRX_LIST; + m_start_trx_id_range = 0; + m_end_trx_id_range = SCAN_RANGE; + m_next_trx_id_range = TRX_ID_MAX; + break; + case SCANNING_TRX_LIST: + m_scan_pass = DONE_SCANNING; + break; +#else case INIT_SCANNING: m_scan_pass = SCANNING_RW_TRX_LIST; m_start_trx_id_range = 0; @@ -217,6 +241,7 @@ class Innodb_trx_scan_state { case SCANNING_MYSQL_TRX_LIST: m_scan_pass = DONE_SCANNING; break; +#endif /* HAVE_LOCK_FREE_READVIEW */ case DONE_SCANNING: default: ut_error; @@ -350,6 +375,41 @@ class Innodb_data_lock_wait_iterator Innodb_trx_scan_state m_scan_state; }; +#ifdef HAVE_LOCK_FREE_READVIEW +/** Check if a transaction should be discarded. +Transactions present in the trx list that have not started yet +are discarded, when inspecting data locks. +@param[in] trx Transaction to evaluate +@returns True if the trx should be discarded +*/ +bool discard_trx(const trx_t *trx) { return !trx_is_started(trx); } + +/** Find a transaction in a TRX LIST. +@param[in] trx_id The transaction id +@param[in] trx_list The transaction list +@returns The transaction when found, or NULL +*/ +static const trx_t *fetch_trx_in_trx_list(uint64_t filter_trx_immutable_id, + trx_ut_list_t *trx_list) { + const trx_t *trx; + + ut_ad(lock_mutex_own()); + ut_ad(trx_sys->mutex.is_owned()); + + for (trx = UT_LIST_GET_FIRST(*trx_list); trx != NULL; + trx = get_next_trx(trx)) { + if (discard_trx(trx)) { + continue; + } + + if (filter_trx_immutable_id == trx_immutable_id(trx)) { + return trx; + } + } + + return nullptr; +} +#else /** Check if a transaction should be discarded. Transactions present in any TRX LIST that have not started yet are discarded, when inspecting data locks. @@ -398,6 +458,7 @@ static const trx_t *fetch_trx_in_trx_list(uint64_t filter_trx_immutable_id, return NULL; } +#endif /* HAVE_LOCK_FREE_READVIEW */ Innodb_data_lock_inspector::Innodb_data_lock_inspector() {} @@ -586,6 +647,16 @@ bool Innodb_data_lock_iterator::scan(PSI_server_data_lock_container *container, lock_mutex_enter(); trx_sys_mutex_enter(); +#ifdef HAVE_LOCK_FREE_READVIEW + + size_t found = 0; + + while ((m_scan_state.get_pass() == SCANNING_TRX_LIST) && (found == 0)) { + found = scan_trx_list(container, with_lock_data, true, &trx_sys->trx_list); + m_scan_state.prepare_next_scan(); + } + +#else size_t found = 0; @@ -601,6 +672,7 @@ bool Innodb_data_lock_iterator::scan(PSI_server_data_lock_container *container, m_scan_state.prepare_next_scan(); } +#endif /* HAVE_LOCK_FREE_READVIEW */ trx_sys_mutex_exit(); lock_mutex_exit(); @@ -632,6 +704,15 @@ bool Innodb_data_lock_iterator::fetch(PSI_server_data_lock_container *container, lock_mutex_enter(); trx_sys_mutex_enter(); +#ifdef HAVE_LOCK_FREE_READVIEW + + trx = fetch_trx_in_trx_list(trx_immutable_id, &trx_sys->trx_list); + + if (trx != nullptr) { + scan_trx(container, with_lock_data, trx, true, lock_immutable_id, heap_id); + } + +#else trx = fetch_trx_in_trx_list(trx_immutable_id, true, &trx_sys->rw_trx_list); @@ -644,6 +725,7 @@ bool Innodb_data_lock_iterator::fetch(PSI_server_data_lock_container *container, scan_trx(container, with_lock_data, trx, true, lock_immutable_id, heap_id); } +#endif /* HAVE_LOCK_FREE_READVIEW */ trx_sys_mutex_exit(); lock_mutex_exit(); @@ -667,6 +749,24 @@ size_t Innodb_data_lock_iterator::scan_trx_list( size_t found = 0; ut_ad(lock_mutex_own()); +#ifdef HAVE_LOCK_FREE_READVIEW + ut_ad(trx_sys->mutex.is_owned()); + + for (trx = UT_LIST_GET_FIRST(*trx_list); trx != nullptr; + trx = get_next_trx(trx)) { + if (discard_trx(trx)) { + continue; + } + + trx_id = trx_get_id_for_print(trx); + + if (!m_scan_state.trx_id_in_range(trx_id)) { + continue; + } + + found += scan_trx(container, with_lock_data, trx, false); + } +#else ut_ad(trx_sys_mutex_own()); for (trx = UT_LIST_GET_FIRST(*trx_list); trx != NULL; @@ -683,6 +783,7 @@ size_t Innodb_data_lock_iterator::scan_trx_list( found += scan_trx(container, with_lock_data, trx, false); } +#endif /* HAVE_LOCK_FREE_READVIEW */ return found; } @@ -859,6 +960,16 @@ bool Innodb_data_lock_wait_iterator::scan( lock_mutex_enter(); trx_sys_mutex_enter(); +#ifdef HAVE_LOCK_FREE_READVIEW + + size_t found = 0; + + while ((m_scan_state.get_pass() == SCANNING_TRX_LIST) && (found == 0)) { + found = scan_trx_list(container, true, &trx_sys->trx_list); + m_scan_state.prepare_next_scan(); + } + +#else size_t found = 0; @@ -872,6 +983,7 @@ bool Innodb_data_lock_wait_iterator::scan( m_scan_state.prepare_next_scan(); } +#endif /* HAVE_LOCK_FREE_READVIEW */ trx_sys_mutex_exit(); lock_mutex_exit(); @@ -918,6 +1030,16 @@ bool Innodb_data_lock_wait_iterator::fetch( lock_mutex_enter(); trx_sys_mutex_enter(); +#ifdef HAVE_LOCK_FREE_READVIEW + + trx = fetch_trx_in_trx_list(requesting_trx_immutable_id, &trx_sys->trx_list); + + if (trx != nullptr) { + scan_trx(container, trx, true, requesting_lock_immutable_id, + blocking_lock_immutable_id); + } + +#else trx = fetch_trx_in_trx_list(requesting_trx_immutable_id, true, &trx_sys->rw_trx_list); @@ -932,6 +1054,7 @@ bool Innodb_data_lock_wait_iterator::fetch( blocking_lock_immutable_id); } +#endif /* HAVE_LOCK_FREE_READVIEW */ trx_sys_mutex_exit(); lock_mutex_exit(); @@ -953,6 +1076,24 @@ size_t Innodb_data_lock_wait_iterator::scan_trx_list( size_t found = 0; ut_ad(lock_mutex_own()); +#ifdef HAVE_LOCK_FREE_READVIEW + ut_ad(trx_sys->mutex.is_owned()); + + for (trx = UT_LIST_GET_FIRST(*trx_list); trx != nullptr; + trx = get_next_trx(trx)) { + if (discard_trx(trx)) { + continue; + } + + trx_id = trx_get_id_for_print(trx); + + if (!m_scan_state.trx_id_in_range(trx_id)) { + continue; + } + + found += scan_trx(container, trx, false); + } +#else ut_ad(trx_sys_mutex_own()); for (trx = UT_LIST_GET_FIRST(*trx_list); trx != NULL; @@ -969,6 +1110,7 @@ size_t Innodb_data_lock_wait_iterator::scan_trx_list( found += scan_trx(container, trx, false); } +#endif /* HAVE_LOCK_FREE_READVIEW */ return found; } diff --git a/storage/innobase/include/clone0repl.h b/storage/innobase/include/clone0repl.h index 625b6e0..92d0275 100644 --- a/storage/innobase/include/clone0repl.h +++ b/storage/innobase/include/clone0repl.h @@ -235,7 +235,9 @@ class Clone_persist_gtid { /** @return current active GTID list */ Gitd_info_list &get_active_list() { +#ifndef HAVE_LOCK_FREE_READVIEW ut_ad(trx_sys_mutex_own()); +#endif return (get_list(m_active_number)); } @@ -292,7 +294,9 @@ class Clone_persist_gtid { /** Switch active GTID list. */ uint64_t switch_active_list() { /* Switch active list under transaction system mutex. */ +#ifndef HAVE_LOCK_FREE_READVIEW ut_ad(trx_sys_mutex_own()); +#endif uint64_t flush_number = m_active_number; ++m_active_number; m_compression_gtid_counter += m_num_gtid_mem; diff --git a/storage/innobase/include/read0read.h b/storage/innobase/include/read0read.h index eab38c4..49cdc79 100644 --- a/storage/innobase/include/read0read.h +++ b/storage/innobase/include/read0read.h @@ -33,6 +33,8 @@ this program; if not, write to the Free Software Foundation, Inc., #ifndef read0read_h #define read0read_h +#ifndef HAVE_LOCK_FREE_READVIEW + #include #include @@ -128,4 +130,5 @@ class MVCC { view_list_t m_views; }; +#endif /* HAVE_LOCK_FREE_READVIEW */ #endif /* read0read_h */ diff --git a/storage/innobase/include/read0types.h b/storage/innobase/include/read0types.h index 973f101..331c304 100644 --- a/storage/innobase/include/read0types.h +++ b/storage/innobase/include/read0types.h @@ -38,13 +38,25 @@ this program; if not, write to the Free Software Foundation, Inc., #include "trx0types.h" +#ifdef HAVE_LOCK_FREE_READVIEW +/** View is not visible to purge thread. */ +constexpr int32_t READ_VIEW_STATE_CLOSED = 0; + +/** View is being opened, purge thread must wait for state change. */ +constexpr int32_t READ_VIEW_STATE_SNAPSHOT = 1; + +/** View is visible to purge thread. */ +constexpr int32_t READ_VIEW_STATE_OPEN = 2; +#else // Friend declaration class MVCC; +#endif /* HAVE_LOCK_FREE_READVIEW */ /** Read view lists the trx ids of those transactions for which a consistent read should not see the modifications to the database. */ class ReadView { +#ifndef HAVE_LOCK_FREE_READVIEW /** This is similar to a std::vector but it is not a drop in replacement. It is specific to ReadView. */ class ids_t { @@ -148,6 +160,7 @@ class ReadView { friend class ReadView; }; +#endif /* HAVE_LOCK_FREE_READVIEW */ public: ReadView(); @@ -178,9 +191,13 @@ class ReadView { return (true); } +#ifdef HAVE_LOCK_FREE_READVIEW + return (!std::binary_search(m_ids.begin(), m_ids.end(), id)); +#else const ids_t::value_type *p = m_ids.data(); return (!std::binary_search(p, p + m_ids.size(), id)); +#endif /* HAVE_LOCK_FREE_READVIEW */ } /** @@ -188,16 +205,62 @@ class ReadView { @return true if view sees transaction id */ bool sees(trx_id_t id) const { return (id < m_up_limit_id); } +#ifdef HAVE_LOCK_FREE_READVIEW + /** Creates a snapshot where exactly the transaction serialized before this + point in time are seen in the view. + + @param[in, out] trx transaction */ + void snapshot(trx_t *trx); + + /** Open a read view where exactly the transaction serialized before this + point in time are seen in the view. + + View become visible to purge thread. + + @param[in,out] trx transaction */ + void open(trx_t *trx); +#endif /* HAVE_LOCK_FREE_READVIEW */ /** Mark the view as closed */ void close() { +#ifdef HAVE_LOCK_FREE_READVIEW + int32_t state = m_state.load(std::memory_order_relaxed); + ut_ad(state == READ_VIEW_STATE_CLOSED || state == READ_VIEW_STATE_OPEN); + if (state == READ_VIEW_STATE_OPEN) { + m_state.store(READ_VIEW_STATE_CLOSED, std::memory_order_relaxed); + } +#else ut_ad(m_creator_trx_id != TRX_ID_MAX); m_creator_trx_id = TRX_ID_MAX; +#endif /* HAVE_LOCK_FREE_READVIEW */ } +#ifdef HAVE_LOCK_FREE_READVIEW + /** m_state getter fir trx_sys::clone_oldest_view() & trx_sys::size(). */ + int32_t get_state() const { return m_state.load(std::memory_order_acquire); } + + /** Returns ture if view is open. + + Only used by view owner thread, thus we can omit atomic operations. */ + bool is_open() const { + int32_t state = m_state.load(std::memory_order_relaxed); + ut_ad(state == READ_VIEW_STATE_OPEN || state == READ_VIEW_STATE_CLOSED); + return (state == READ_VIEW_STATE_OPEN); + } + + /** Set the creator transaction id. + + This should be set only for views created by RW transactions. */ + void set_creator_trx_id(trx_id_t id) { + ut_ad(id > 0); + ut_ad(m_creator_trx_id == 0); + m_creator_trx_id = id; + } +#else /** @return true if the view is closed */ bool is_closed() const { return (m_closed); } +#endif /* HAVE_LOCK_FREE_READVIEW */ /** Write the limits to the file. @@ -244,7 +307,11 @@ class ReadView { return (m_low_limit_no <= rhs->m_low_limit_no); } #endif /* UNIV_DEBUG */ +#ifdef HAVE_LOCK_FREE_READVIEW + void copy(const ReadView &other); +#endif /* HAVE_LOCK_FREE_READVIEW */ private: +#ifndef HAVE_LOCK_FREE_READVIEW /** Copy the transaction ids from the source vector */ inline void copy_trx_ids(const trx_ids_t &trx_ids); @@ -273,6 +340,7 @@ class ReadView { } friend class MVCC; +#endif /* HAVE_LOCK_FREE_READVIEW */ private: // Disable copying @@ -295,7 +363,11 @@ class ReadView { /** Set of RW transactions that was active when this snapshot was taken */ +#ifdef HAVE_LOCK_FREE_READVIEW + trx_ids_t m_ids; +#else ids_t m_ids; +#endif /* HAVE_LOCK_FREE_READVIEW */ /** The view does not need to see the undo logs for transactions whose transaction number is strictly smaller (<) than this value: @@ -310,6 +382,25 @@ class ReadView { trx_id_t m_view_low_limit_no; #endif /* UNIV_DEBUG */ +#ifdef HAVE_LOCK_FREE_READVIEW + /** View state. + + It is not defined as enum as it has to be updated using atomic operations. + Possible values are READ_VIEW_STATE_CLOSED, READ_VIEW_STATE_SNAPSHOT and + READ_VIEW_STATE_OPEN. + + Possible state transfers... + + Start view open: + READ_VIEW_STATE_CLOSED -> READ_VIEW_STATE_SNAPSHOT + + Complete view open: + READ_VIEW_STATE_SNAPSHOT -> READ_VIEW_STATE_OPEN + + Close view: + READ_VIEW_STATE_OPEN -> READ_VIEW_STATE_CLOSED */ + std::atomic m_state; +#else /** AC-NL-RO transaction view that has been "closed". */ bool m_closed; @@ -318,6 +409,7 @@ class ReadView { /** List of read views in trx_sys */ byte pad1[64 - sizeof(node_t)]; node_t m_view_list; +#endif /* HAVE_LOCK_FREE_READVIEW */ }; #endif diff --git a/storage/innobase/include/row0vers.h b/storage/innobase/include/row0vers.h index e8bf136..68fde81 100644 --- a/storage/innobase/include/row0vers.h +++ b/storage/innobase/include/row0vers.h @@ -53,6 +53,9 @@ class ReadView; negatives. The caller must confirm all positive results by calling trx_is_active() while holding lock_sys->mutex. */ trx_t *row_vers_impl_x_locked( +#ifdef HAVE_LOCK_FREE_READVIEW + trx_t *caller_trx, /*!< in/out: trx of current thread */ +#endif /* HAVE_LOCK_FREE_READVIEW */ const rec_t *rec, /*!< in: record in a secondary index */ const dict_index_t *index, /*!< in: the secondary index */ const ulint *offsets); /*!< in: rec_get_offsets(rec, index) */ @@ -116,6 +119,9 @@ dberr_t row_vers_build_for_consistent_read( /** Constructs the last committed version of a clustered index record, which should be seen by a semi-consistent read. */ void row_vers_build_for_semi_consistent_read( +#ifdef HAVE_LOCK_FREE_READVIEW + trx_t *caller_trx, /*!< in/out: trx of current thread */ +#endif /* HAVE_LOCK_FREE_READVIEW */ const rec_t *rec, /*!< in: record in a clustered index; the caller must have a latch on the page; this latch locks the top of the stack of versions diff --git a/storage/innobase/include/srv0srv.h b/storage/innobase/include/srv0srv.h index 738f486..90a34d6 100644 --- a/storage/innobase/include/srv0srv.h +++ b/storage/innobase/include/srv0srv.h @@ -1149,6 +1149,9 @@ struct export_var_t { the dba created explicitly. */ ulint innodb_undo_tablespaces_active; /*!< number of active undo tablespaces */ +#ifdef HAVE_LOCK_FREE_READVIEW + bool innodb_have_lock_free_readview; +#endif #ifdef UNIV_DEBUG ulint innodb_purge_trx_id_age; /*!< rw_max_trx_no - purged trx_no */ ulint innodb_purge_view_trx_id_age; /*!< rw_max_trx_no diff --git a/storage/innobase/include/sync0sync.h b/storage/innobase/include/sync0sync.h index ed8449e..85b9fea 100644 --- a/storage/innobase/include/sync0sync.h +++ b/storage/innobase/include/sync0sync.h @@ -158,6 +158,9 @@ extern mysql_pfs_key_t trx_pool_manager_mutex_key; extern mysql_pfs_key_t temp_pool_manager_mutex_key; extern mysql_pfs_key_t lock_mutex_key; extern mysql_pfs_key_t lock_wait_mutex_key; +#ifdef HAVE_LOCK_FREE_READVIEW +extern mysql_pfs_key_t rw_trx_hash_element_mutex_key; +#endif /* HAVE_LOCK_FREE_READVIEW */ extern mysql_pfs_key_t trx_sys_mutex_key; extern mysql_pfs_key_t srv_sys_mutex_key; extern mysql_pfs_key_t srv_threads_mutex_key; diff --git a/storage/innobase/include/sync0types.h b/storage/innobase/include/sync0types.h index 0ee4353..b925b8a 100644 --- a/storage/innobase/include/sync0types.h +++ b/storage/innobase/include/sync0types.h @@ -267,6 +267,9 @@ enum latch_level_t { SYNC_REC_LOCK, SYNC_THREADS, SYNC_TRX, +#ifdef HAVE_LOCK_FREE_READVIEW + SYNC_RW_TRX_HASH_ELEMENT, +#endif /* HAVE_LOCK_FREE_READVIEW */ SYNC_POOL, SYNC_POOL_MANAGER, SYNC_TRX_SYS, @@ -452,6 +455,9 @@ enum latch_id_t { LATCH_ID_CLONE_SYS, LATCH_ID_CLONE_TASK, LATCH_ID_CLONE_SNAPSHOT, +#ifdef HAVE_LOCK_FREE_READVIEW + LATCH_ID_RW_TRX_HASH_ELEMENT, +#endif /* HAVE_LOCK_FREE_READVIEW */ LATCH_ID_PARALLEL_READ, LATCH_ID_REDO_LOG_ARCHIVE_ADMIN_MUTEX, LATCH_ID_REDO_LOG_ARCHIVE_QUEUE_MUTEX, diff --git a/storage/innobase/include/trx0roll.h b/storage/innobase/include/trx0roll.h index 83c13a9..b6312ed 100644 --- a/storage/innobase/include/trx0roll.h +++ b/storage/innobase/include/trx0roll.h @@ -61,6 +61,15 @@ trx_undo_rec_t *trx_roll_pop_top_rec_of_trx( roll_ptr_t *roll_ptr, /*!< out: roll pointer to undo record */ mem_heap_t *heap); /*!< in: memory heap where copied */ +#ifdef HAVE_LOCK_FREE_READVIEW +/** Rollback or clean up any incomplete transactions which were + encountered in crash recovery. If the transaction already was + committed, then we clean up a possible insert undo log. If the + transaction was not yet committed, then we roll it back. + @param all true=roll back all recovered active transactions; + false=roll back any incomplete dictionary transaction */ +void trx_rollback_recovered(bool all); +#else /** Rollback or clean up any incomplete transactions which were encountered in crash recovery. If the transaction already was committed, then we clean up a possible insert undo log. If the @@ -68,6 +77,7 @@ trx_undo_rec_t *trx_roll_pop_top_rec_of_trx( void trx_rollback_or_clean_recovered( ibool all); /*!< in: FALSE=roll back dictionary transactions; TRUE=roll back all non-PREPARED transactions */ +#endif /* HAVE_LOCK_FREE_READVIEW */ /** Rollback or clean up any incomplete transactions which were encountered in crash recovery. If the transaction already was diff --git a/storage/innobase/include/trx0sys.h b/storage/innobase/include/trx0sys.h index d7cc4ed..eafe593 100644 --- a/storage/innobase/include/trx0sys.h +++ b/storage/innobase/include/trx0sys.h @@ -35,6 +35,10 @@ this program; if not, write to the Free Software Foundation, Inc., #include "univ.i" +#ifdef HAVE_LOCK_FREE_READVIEW +#include "lf.h" +#endif /* HAVE_LOCK_FREE_READVIEW */ + #include "buf0buf.h" #include "fil0fil.h" #include "trx0types.h" @@ -54,7 +58,9 @@ this program; if not, write to the Free Software Foundation, Inc., typedef UT_LIST_BASE_NODE_T(trx_t) trx_ut_list_t; // Forward declaration +#ifndef HAVE_LOCK_FREE_READVIEW class MVCC; +#endif /* HAVE_LOCK_FREE_READVIEW */ class ReadView; /** The transaction system */ @@ -130,6 +136,7 @@ UNIV_INLINE void trx_sysf_rseg_set_page_no(trx_sysf_t *sys_header, ulint i, page_no_t page_no, mtr_t *mtr); +#ifndef HAVE_LOCK_FREE_READVIEW /** Allocates a new transaction id. @return new, allocated trx id */ UNIV_INLINE @@ -139,6 +146,7 @@ trx_id_t trx_sys_get_new_trx_id(); next call to trx_sys_get_new_trx_id() */ UNIV_INLINE trx_id_t trx_sys_get_max_trx_id(void); +#endif /* HAVE_LOCK_FREE_READVIEW */ #ifdef UNIV_DEBUG /* Flag to control TRX_RSEG_N_SLOTS behavior debugging. */ @@ -162,6 +170,7 @@ UNIV_INLINE trx_id_t trx_read_trx_id( const byte *ptr); /*!< in: pointer to memory from where to read */ +#ifndef HAVE_LOCK_FREE_READVIEW /** Looks for the trx instance with the given id in the rw trx_list. @return the trx handle or NULL if not found */ UNIV_INLINE @@ -206,6 +215,7 @@ UNIV_INLINE ibool trx_assert_recovered(trx_id_t trx_id) /*!< in: transaction identifier */ MY_ATTRIBUTE((warn_unused_result)); #endif /* UNIV_DEBUG || UNIV_BLOB_LIGHT_DEBUG */ +#endif /* HAVE_LOCK_FREE_READVIEW */ /** Persist transaction number limit below which all transaction GTIDs are persisted to disk table. @@ -246,6 +256,7 @@ void trx_sys_update_mysql_binlog_offset(trx_t *trx, mtr_t *mtr); /** Shutdown/Close the transaction system. */ void trx_sys_close(void); +#ifndef HAVE_LOCK_FREE_READVIEW /** Determine if there are incomplete transactions in the system. @return whether incomplete transactions need rollback */ UNIV_INLINE @@ -255,7 +266,9 @@ bool trx_sys_need_rollback(); Check if there are any active (non-prepared) transactions. @return total number of active transactions or 0 if none */ ulint trx_sys_any_active_transactions(void); +#endif /* HAVE_LOCK_FREE_READVIEW */ #endif /* !UNIV_HOTBACKUP */ +#ifndef HAVE_LOCK_FREE_READVIEW /** Add the transaction to the RW transaction set @param trx transaction instance to add */ @@ -267,6 +280,7 @@ void trx_sys_rw_trx_add(trx_t *trx); @return true if the list is valid */ bool trx_sys_validate_trx_list(); #endif /* UNIV_DEBUG */ +#endif /* HAVE_LOCK_FREE_READVIEW */ /** Initialize trx_sys_undo_spaces, called once during srv_start(). */ void trx_sys_undo_spaces_init(); @@ -420,6 +434,509 @@ class Space_Ids : public std::vector> { }; #ifndef UNIV_HOTBACKUP +#ifdef HAVE_LOCK_FREE_READVIEW +trx_t *current_trx(); + +struct rw_trx_hash_element_t { + rw_trx_hash_element_t() : trx(nullptr) { + mutex_create(LATCH_ID_RW_TRX_HASH_ELEMENT, &mutex); + } + + ~rw_trx_hash_element_t() { mutex_free(&mutex); } + + /* lf_hash_init() relies on this to be first in the struct. */ + trx_id_t id = 0; + + std::atomic no; + + trx_t *trx; + + ib_mutex_t mutex; +}; + +/** Wrapper around LF_HASH to store set of in-memory read-write transactions. */ +class rw_trx_hash_t { + LF_HASH hash; + + /** Constructor callback for lock-free allocator. + + Object is just allocated and is not yet accessible via rw_trx_hash by + concurrent threads. Object can be reused multiple times before it is freed. + Every time object is being reused initialize() callback is called. */ + static void rw_trx_hash_constructor(uchar *arg); + + /** Destructor callback for lock-free allocator. + + Object is about to be freed and is not accessible via rw_trx_hash by + concurrent threads. */ + static void rw_trx_hash_destructor(uchar *arg); + + /** Destructor callback for lock-free allocator. + + This destructor is used at shutdown. It frees remaining transaction objects. + + XA PREPARED transactions may remain if they haven't been committed or rolled + back. ACTIVE transactions may remain if startup was interrupted or server is + running in read-only mode or for certain srv_force_recovery levels. */ + static void rw_trx_hash_shutdown_destructor(uchar *arg); + + /** Initializer callback for lock-free hash. + + Object is not yet accessible via rw_trx_hash by concurrent threads, but is + about to become such. Object id can be changed only by this callback and + remains the same until all pins to this object are released. + + Object trx can be changed to 0 by erase() under object mutex protection, + which indicates it is about to be removed from lock-free hash and become not + accessible by concurrent threads. */ + static void rw_trx_hash_initialize(rw_trx_hash_element_t *element, + trx_t *trx); + + /** Gets LF_HASH pins. + + Pins are used to protect object from being destroyed or reused. They are + normally stored in trx object for quick access. If caller doesn't have trx + available, we try to get it using current_trx(). If caller doesn't have trx at + all, temporary pins are allocated. */ + LF_PINS *get_pins(trx_t *trx); + + struct eliminate_duplicates_arg { + trx_ids_t ids; + lf_hash_walk_func *action; + void *argument; + + eliminate_duplicates_arg(size_t size, lf_hash_walk_func *act, void *arg) + : action(act), argument(arg) { + ids.reserve(size); + } + }; + + static bool eliminate_duplicates(rw_trx_hash_element_t *element, + eliminate_duplicates_arg *arg); + +#ifdef UNIV_DEBUG + static void validate_element(trx_t *trx); + + struct debug_iterator_arg { + lf_hash_walk_func *action; + void *argument; + }; + + static bool debug_iterator(rw_trx_hash_element_t *element, + debug_iterator_arg *arg); +#endif /* UNIV_DEBUG */ + + public: + void init(); + + void destroy(); + + /** Releases LF_HASH pins. + + Must be called by thread that owns trx_t object when the later is being + "detached" from thread (e.g. released to the pool by trx_free()). Can be + called earlier if thread is expected not to use rw_trx_hash. + + Since pins are not allowed to be transferred to another thread, + initialisation thread calls this for recovered transactions. */ + void put_pins(trx_t *trx); + + /** Finds trx object in lock-free hash with given id. + + Only ACTIVE or PREPARED trx objects may participate in hash. Nevertheless the + transaction may get committed before this method returns. + + With do_ref_count == false the caller may dereference returned trx pointer + only if lock_sys.mutex was acquired before calling find(). + + With do_ref_count == true caller dereferemce trx even if it is not holding + lock_sys.mutex. Caller is responsible for calling trx->release_reference() + when it is done playing with trx. + + Ideally this method should get caller rw_trx_hash_pins along with trx object + as a parameter, similar to insert() and erase(). However most callers lose trx + early in their call chains and it is not that easy to pass them through. + + So we take more expensive approach: get trx through current_thd()->ha_data. + Some threads don't have trx attached to THD, and at least server + initialisation thread, fts_optimize_thread, srv_master_thread, + dict_stats_thread, srv_monitor_thread, btr_defragment_thread don't even have + THD at all. For such cases we allocate pins only for duration of search and + free them immediately. + + This has negative performance impact and should be fixed eventually (by + passing caller_trx as a parameter). Still stream of DML is more or less Ok. + + @return pointer to trx or nullptr if not found */ + trx_t *find(trx_t *caller_trx, trx_id_t trx_id, bool do_ref_count); + + /** Inserts trx to lock-free hash. + + Object becomes accessible via rw_trx_hash. */ + void insert(trx_t *trx); + + /** Removes trx from lock-free hash. + + Object becomes not accessible via rw_trx_hash. But it still can be pinned by + concurrent find(), which is supposed to release it immediately after it sees + object trx is nullptr. */ + void erase(trx_t *trx); + + /** Returns the number of elements in the hash. + + The number is exact only if hash is protected against concurrent modifications + (e.g., single threaded startup or hash is protected by some mutex). Otherwise + the number maybe used as a hint only, because it may change even before this + method returns. */ + uint32_t size(); + + /** Iterates the hash. + + @param caller_trx used to get/set pins + @param action called for every element in hash + @param argument opque argument passed to action + + May return the same element multiple times if hash is under contention. If + caller doesn't like to see the same transaction multiple times, it has to call + iterate_no_dups() instead. + + May return element with committed transaction. If caller doesn't like to see + committed transactions, it has to skip those under element mutex: + + mutex_enter(&element->mutex); + trx_t *trx = element->trx; + if (trx != nullptr) { + // trx is protected against commit in this branch + } + mutex_exit(&element->mutex); + + May miss concurrently inserted transactions. + + @return 0 if iteration completed successfuly, or 1 if iteration was + interrupted (action returned true) */ + int iterate(trx_t *caller_trx, const lf_hash_walk_func *action, const void *argument); + + int iterate(const lf_hash_walk_func *action, const void *argument); + + /** Iterates the hash and eliminates duplicate elements. + + @sa iterate() */ + int iterate_no_dups(trx_t *caller_trx, lf_hash_walk_func *action, + void *argument); + + int iterate_no_dups(lf_hash_walk_func *action, void *argument); +}; +#endif /* HAVE_LOCK_FREE_READVIEW */ + +#ifdef HAVE_LOCK_FREE_READVIEW +/** The transaction system central memory data structure. */ +struct trx_sys_t { + private: + /** To avoid false sharing */ + char pad1[INNOBASE_CACHE_LINE_SIZE]; + /** The smallest number not yet assigned as a transaction id or transaction + number. Accessed and updated with atomic operations. */ + std::atomic max_trx_id; + + /** To avoid false sharing */ + char pad2[INNOBASE_CACHE_LINE_SIZE]; + /** Solves race conditions between register_rw() and snapshot_ids() as well as + race condition between assign_new_trx_no() and snapshot_ids(). + + @sa register_rw() + @sa assign_new_trx_no() + @sa snapshot_ids() */ + std::atomic rw_trx_hash_version; + + public: + /** To avoid false sharing */ + char pad3[INNOBASE_CACHE_LINE_SIZE]; + /** Mutex protecting trx list. */ + mutable TrxSysMutex mutex; + + /** To avoid false sharing */ + char pad4[INNOBASE_CACHE_LINE_SIZE]; + /** List of all transactions. */ + trx_ut_list_t trx_list; + + /** To avoid false sharing */ + char pad5[INNOBASE_CACHE_LINE_SIZE]; + /** Lock-free hash of in-memory read-write transactions. Works faster when + it's on it's own cache line (tested). */ + rw_trx_hash_t rw_trx_hash; + + char pad6[INNOBASE_CACHE_LINE_SIZE]; /*!< To avoid false sharing */ + + Rsegs rsegs; /*!< Vector of pointers to rollback + segments. These rsegs are iterated + and added to the end under a read + lock. They are deleted under a write + lock while the vector is adjusted. + They are created and destroyed in + single-threaded mode. */ + + Rsegs tmp_rsegs; /*!< Vector of pointers to rollback + segments within the temp tablespace; + This vector is created and destroyed + in single-threaded mode so it is not + protected by any mutex because it is + read-only during multi-threaded + operation. */ + + std::atomic rseg_history_len; + /*!< Length of the TRX_RSEG_HISTORY + list (update undo logs for committed + transactions), protected by + rseg->mutex */ + + /** To avoid false sharing */ + char pad7[INNOBASE_CACHE_LINE_SIZE]; +#ifdef UNIV_DEBUG + std::atomic rw_max_trx_no; /*!< Max trx number of read-write + transactions added for purge. */ +#endif /* UNIV_DEBUG */ + + /** Returns the minimum trx id in rw trx list. + + This is the smallest id for which the trx can possibly be active. (But, you + must look at trx->state to find out if the minimum trx id transaction itself + is active, or already committed. + + @return the minimum trx id, or max_trx_id if the trx list is empty */ + trx_id_t get_min_trx_id() { + trx_id_t id = get_max_trx_id(); + rw_trx_hash.iterate( + reinterpret_cast(get_min_trx_id_callback), &id); + return id; + } + + /** Determines the maximum transaction id. + + @return maximum currently allocated trx id; will be stale after the next call + to trx_sys.assign_new_trx_no */ + trx_id_t get_max_trx_id() { + return max_trx_id.load(std::memory_order_relaxed); + } + + /** Allocates and assigns new transaction serialisation number. + + There's a gap between max_trx_id increment and transaction serialisation + number becoming visible through rw_trx_hash. While we're in this gap + concurrent thread may come and do MVCC snapshot without seeing allocated but + not yet assigned serialisation number. Then at some point purge thread may + clone this view. As a result it won't see newly allocated serialisation number + and may remove "unnecessary" history data of this transaction from rollback + segments. + + rw_trx_hash_version is intended to solve this problem. MVCC snapshot has to + wait until max_trx_id == rw_trx_hash_version, which effectively means that all + transaction serialisation numbers up to max_trx_id are available through + rw_trx_hash. + + We rely on refresh_rw_trx_hash_version() to issue RELEASE memory barrier so + that rw_trx_hash_version increment happens after trx->rw_trx_hash_element->no + becomes available visible through rw_trx_hash. + + @param trx transaction */ + void assign_new_trx_no(trx_t *trx) { + trx->no = get_new_trx_id_no_refresh(); + trx->rw_trx_hash_element->no.store(trx->no, std::memory_order_relaxed); + refresh_rw_trx_hash_version(); + } + + /** Takes MVCC snapshot. + + To reduce malloc probability we reserve rw_trx_hash.size() + 32 elements in + ids. + + For details about get_rw_trx_hash_version() != get_max_trx_id() spin + @sa register_rw() and @sa assign_new_trx_no(). + + We rely on get_rw_trx_hash_version() to issue ACQUIRE memory barrier so that + loading of rw_trx_hash_version happens before accessing rw_trx_hash. + + To optimise snapshot creation rw_trx_hash.iterate is being used instead of + rw_trx_hash.iterate_no_dups(). It means that some transaction identifiers may + appear multiple times in ids. + + @param[in,out] caller_trx used to get access to rw_trx_hash_pins + @param[out] ids array to store registered transaction identifiers + @param[out] max_trx_id variable to store max_trx_id value + @param[out] mix_trx_no variable to store min(trx->no) value */ + void snapshot_ids(trx_t *caller_trx, trx_ids_t *ids, trx_id_t *max_trx_id, + trx_id_t *min_trx_no) { + ut_ad(!mutex_own(&mutex)); + snapshot_ids_arg arg(ids); + + while ((arg.id = get_rw_trx_hash_version()) != get_max_trx_id()) { + ut_delay(1); + } + arg.no = arg.id; + + uint32_t hashSize = rw_trx_hash.size(); + ids->clear(); + + if (hashSize != 0) { + ids->reserve(hashSize + 32); + rw_trx_hash.iterate( + caller_trx, reinterpret_cast(copy_one_id), &arg); + } + + *max_trx_id = arg.id; + *min_trx_no = arg.no; + } + + /** Initialiser for max_trx_id and rw_trx_hash_version. */ + void init_max_trx_id(trx_id_t value) { + max_trx_id = rw_trx_hash_version = value; + } + + /** @return total number of active (non-prepared) transactions */ + ulint any_active_transactions(); + + /** Registers read-write transaction. + + Transaction becomes visible to MVCC. + + There's a gap between max_trx_id increment and transaction becoming visible + through rw_trx_hash. While we're in this gap concurrent thread may come and do + MVCC snapshot. As a result concurrent readview will be able to observe records + owned by this transaction even before it is committed. + + rw_trx_hash_version is intendded to solve this problem. MVCC snapshot has to + wait until max_trx_id == rw_trx_hash_version, which effectively means that all + transactions up to max_trx_id are available through rw_trx_hash. + + We rely on refresh_rw_trx_hash_version() to issue RELEASE memory barrier so + that rw_trx_hash_version increment happens after transaction becomes visible + through rw_trx_hash. */ + void register_rw(trx_t *trx) { + trx->id = get_new_trx_id_no_refresh(); + rw_trx_hash.insert(trx); + refresh_rw_trx_hash_version(); + } + + /** For replica only, registers a faked read-write transaction. */ + void register_rw_replica(trx_t *trx) { + /* trx->id and trx_sys->max_trx_id were already set. */ + rw_trx_hash.insert(trx); + rw_trx_hash_version.store(get_max_trx_id(), std::memory_order_release); + } + + /** Deregisters read-write transaction. + + Transaction is removed from rw_trx_hash, which releases all implicit locks. + MVCC snapshot won't see this transaction anymore. */ + void deregister_rw(trx_t *trx) { rw_trx_hash.erase(trx); } + + bool is_registered(trx_t *caller_trx, trx_id_t id) { + return (id > 0) && (find(caller_trx, id, false) != nullptr); + } + + trx_t *find(trx_t *caller_trx, trx_id_t id, bool do_ref_count = true) { + return rw_trx_hash.find(caller_trx, id, do_ref_count); + } + + /** Registers transaction in trx_sys. + + @param trx transaction */ + void register_trx(trx_t *trx) { + mutex_enter(&mutex); + UT_LIST_ADD_FIRST(trx_list, trx); + mutex_exit(&mutex); + } + + /** Deregisters transaction in trx_sys. + + @param trx transaction */ + void deregister_trx(trx_t *trx) { + mutex_enter(&mutex); + UT_LIST_REMOVE(trx_list, trx); + mutex_exit(&mutex); + } + + /** Clones the oldest view and stores it in view. + + No need to call ReadView::close(). The caller owns the view that is passed in. + This function is called by purge thread to determine whether it should purge + the delete marked record or not. */ + void clone_oldest_view(ReadView *view = NULL); + + /** @return the number of active views. */ + size_t view_count() const { + size_t count = 0; + + mutex_enter(&mutex); + for (const trx_t *trx = UT_LIST_GET_FIRST(trx_list); trx != nullptr; + trx = UT_LIST_GET_NEXT(trx_list, trx)) { + if (trx->read_view.get_state() == READ_VIEW_STATE_OPEN) { + ++count; + } + } + mutex_exit(&mutex); + return count; + } + + /** @return true if found prepared transaction(s). */ + bool found_prepared_trx(); + + private: + static bool get_min_trx_id_callback(rw_trx_hash_element_t *element, + trx_id_t *id) { + if (element->id < *id) { + mutex_enter(&element->mutex); + /* We don't care about read-only transactions here. */ + if (element->trx != nullptr && + element->trx->rsegs.m_redo.rseg != nullptr) { + *id = element->id; + } + mutex_exit(&element->mutex); + } + return false; + } + + struct snapshot_ids_arg { + snapshot_ids_arg(trx_ids_t *_ids) : ids(_ids), id(0), no(0) {} + + trx_ids_t *ids; + trx_id_t id; + trx_id_t no; + }; + + static bool copy_one_id(rw_trx_hash_element_t *element, + snapshot_ids_arg *arg) { + if (element->id < arg->id) { + trx_id_t no = element->no.load(std::memory_order_relaxed); + arg->ids->push_back(element->id); + if (no < arg->no) { + arg->no = no; + } + } + return false; + } + + /** Get for rw_trx_hash_version, must issue ACQUIRE memory barrier. */ + trx_id_t get_rw_trx_hash_version() { + return rw_trx_hash_version.load(std::memory_order_acquire); + } + + /** Increments rw_trx_hash_version, must issue RELEASE memory barrier. */ + void refresh_rw_trx_hash_version() { + rw_trx_hash_version.fetch_add(1, std::memory_order_release); + } + + /** Allocates new transaction id without refreshing rw_trx_hash_version. + + This method is extracted for exclusive use by register_rw() and + assign_new_trx_no() where new id must be allocated atomically with payload + of these methods from MVCC snapshot point of view. + + @sa assign_new_trx_no() + + @return new transaction id */ + trx_id_t get_new_trx_id_no_refresh(); +}; +#else /** The transaction system central memory data structure. */ struct trx_sys_t { TrxSysMutex mutex; /*!< mutex protecting most fields in @@ -504,6 +1021,7 @@ struct trx_sys_t { bool found_prepared_trx; /*!< True if XA PREPARED trxs are found. */ }; +#endif /* HAVE_LOCK_FREE_READVIEW */ #endif /* !UNIV_HOTBACKUP */ diff --git a/storage/innobase/include/trx0sys.ic b/storage/innobase/include/trx0sys.ic index cb012de..db43007 100644 --- a/storage/innobase/include/trx0sys.ic +++ b/storage/innobase/include/trx0sys.ic @@ -179,6 +179,7 @@ trx_id_t trx_read_trx_id( return (mach_read_from_6(ptr)); } +#ifndef HAVE_LOCK_FREE_READVIEW /** Looks for the trx handle with the given id in rw_trx_list. The caller must be holding trx_sys->mutex. @return the trx handle or NULL if not found; @@ -380,4 +381,5 @@ void trx_sys_rw_trx_add(trx_t *trx) { ut_d(trx->in_rw_trx_list = true); } +#endif /* HAVE_LOCK_FREE_READVIEW */ #endif /* !UNIV_HOTBACKUP */ diff --git a/storage/innobase/include/trx0trx.h b/storage/innobase/include/trx0trx.h index 9574424..38ed074 100644 --- a/storage/innobase/include/trx0trx.h +++ b/storage/innobase/include/trx0trx.h @@ -45,6 +45,9 @@ this program; if not, write to the Free Software Foundation, Inc., #include "log0log.h" #include "mem0mem.h" #include "que0types.h" +#ifdef HAVE_LOCK_FREE_READVIEW +#include "read0types.h" +#endif /* HAVE_LOCK_FREE_READVIEW */ #include "trx0xa.h" #include "usr0types.h" #include "ut0vec.h" @@ -56,12 +59,22 @@ this program; if not, write to the Free Software Foundation, Inc., // Forward declaration struct mtr_t; +#ifndef HAVE_LOCK_FREE_READVIEW // Forward declaration class ReadView; +#endif /* HAVE_LOCK_FREE_READVIEW */ // Forward declaration class FlushObserver; +#ifdef HAVE_LOCK_FREE_READVIEW +// Forward declaration +struct rw_trx_hash_element_t; + +// Forward declaration +struct LF_PINS; +#endif /* HAVE_LOCK_FREE_READVIEW */ + /** Dummy session used currently in MySQL interface */ extern sess_t *trx_dummy_sess; @@ -94,16 +107,27 @@ trx_t *trx_allocate_for_background(void); /** Resurrect table locks for resurrected transactions. */ void trx_resurrect_locks(); +#ifdef HAVE_LOCK_FREE_READVIEW +/** Release a trx_t instance back to the pool. +@param[in,out] trx the instance to release */ +void trx_free(trx_t *&trx); +#else /** Free and initialize a transaction object instantiated during recovery. @param[in,out] trx transaction object to free and initialize */ void trx_free_resurrected(trx_t *trx); +#endif /* HAVE_LOCK_FREE_READVIEW */ /** Free a transaction that was allocated by background or user threads. @param[in,out] trx transaction object to free */ void trx_free_for_background(trx_t *trx); +#ifdef HAVE_LOCK_FREE_READVIEW +/** At shutdown, frees a transaction object. */ +void trx_free_at_shutdown(trx_t *trx); +#else /** At shutdown, frees a transaction object that is in the PREPARED state. */ void trx_free_prepared(trx_t *trx); /*!< in, own: trx object */ +#endif /* HAVE_LOCK_FREE_READVIEW */ /** Free a transaction object for MySQL. @param[in,out] trx transaction */ @@ -187,10 +211,12 @@ void trx_commit_low( trx_t *trx, /*!< in/out: transaction */ mtr_t *mtr); /*!< in/out: mini-transaction (will be committed), or NULL if trx made no modifications */ +#ifndef HAVE_LOCK_FREE_READVIEW /** Cleans up a transaction at database startup. The cleanup is needed if the transaction already got to the middle of a commit when the database crashed, and we cannot roll it back. */ void trx_cleanup_at_db_startup(trx_t *trx); /*!< in: transaction */ +#endif /* HAVE_LOCK_FREE_READVIEW */ /** Does the transaction commit for MySQL. @return DB_SUCCESS or error number */ dberr_t trx_commit_for_mysql(trx_t *trx); /*!< in/out: transaction */ @@ -220,10 +246,12 @@ trx_t *trx_get_trx_by_xid( void trx_commit_complete_for_mysql(trx_t *trx); /*!< in/out: transaction */ /** Marks the latest SQL statement ended. */ void trx_mark_sql_stat_end(trx_t *trx); /*!< in: trx handle */ +#ifndef HAVE_LOCK_FREE_READVIEW /** Assigns a read view for a consistent read query. All the consistent reads within the same transaction will get the same read view, which is created when this function is first called for a new started transaction. */ ReadView *trx_assign_read_view(trx_t *trx); /*!< in: active transaction */ +#endif /* HAVE_LOCK_FREE_READVIEW */ /** @return the transaction's read view or NULL if one not assigned. */ UNIV_INLINE @@ -361,6 +389,7 @@ tagged as such. @param[in,out] trx Transaction that needs to be "upgraded" to RW from RO */ void trx_set_rw_mode(trx_t *trx); +#ifndef HAVE_LOCK_FREE_READVIEW /** Increase the reference count. If the transaction is in state TRX_STATE_COMMITTED_IN_MEMORY then the transaction is considered @@ -380,6 +409,7 @@ void trx_release_reference(trx_t *trx); /** Check if the transaction is being referenced. */ #define trx_is_referenced(t) ((t)->n_ref > 0) +#endif /* HAVE_LOCK_FREE_READVIEW */ /** @param[in] requestor Transaction requesting the lock @@ -442,6 +472,7 @@ with an explicit check for the read-only status. #define trx_is_ac_nl_ro(t) \ ((t)->read_only && trx_is_autocommit_non_locking((t))) +#ifndef HAVE_LOCK_FREE_READVIEW /** Assert that the transaction is in the trx_sys_t::rw_trx_list */ #define assert_trx_in_rw_list(t) \ @@ -451,6 +482,7 @@ Assert that the transaction is in the trx_sys_t::rw_trx_list */ !((t)->read_only || !(t)->rsegs.m_redo.rseg)); \ check_trx_state(t); \ } while (0) +#endif /* HAVE_LOCK_FREE_READVIEW */ /** Check transaction state */ @@ -472,6 +504,18 @@ Check transaction state */ /** Check if transaction is free so that it can be re-initialized. @param t transaction handle */ +#ifdef HAVE_LOCK_FREE_READVIEW +#define assert_trx_is_free(t) \ + do { \ + ut_ad(trx_state_eq((t), TRX_STATE_NOT_STARTED) || \ + trx_state_eq((t), TRX_STATE_FORCED_ROLLBACK)); \ + ut_ad(!trx_is_rseg_updated(trx)); \ + ut_ad(!(t)->read_view.is_open()); \ + ut_ad((t)->lock.wait_thr == nullptr); \ + ut_ad(UT_LIST_GET_LEN((t)->lock.trx_locks) == 0); \ + ut_ad((t)->dict_operation == TRX_DICT_OP_NONE); \ + } while (0) +#else #define assert_trx_is_free(t) \ do { \ ut_ad(trx_state_eq((t), TRX_STATE_NOT_STARTED) || \ @@ -482,6 +526,7 @@ Check transaction state */ ut_ad(UT_LIST_GET_LEN((t)->lock.trx_locks) == 0); \ ut_ad((t)->dict_operation == TRX_DICT_OP_NONE); \ } while (0) +#endif /* HAVE_LOCK_FREE_READVIEW */ /** Check if transaction is in-active so that it can be freed and put back to transaction pool. @@ -493,6 +538,25 @@ transaction pool. } while (0) #ifdef UNIV_DEBUG +#ifdef HAVE_LOCK_FREE_READVIEW +/** Assert that an autocommit non-locking select cannot be in the + rw_trx_hash and that it is a read-only transaction. + The tranasction must have mysql_thd assigned. */ +#define assert_trx_nonlocking_or_in_list(t) \ + do { \ + if (trx_is_autocommit_non_locking(t)) { \ + trx_state_t t_state = (t)->state; \ + ut_ad((t)->read_only); \ + ut_ad(!(t)->is_recovered); \ + ut_ad((t)->mysql_thd != nullptr); \ + ut_ad(t_state == TRX_STATE_NOT_STARTED || \ + t_state == TRX_STATE_FORCED_ROLLBACK || \ + t_state == TRX_STATE_ACTIVE); \ + } else { \ + check_trx_state(t); \ + } \ + } while (0) +#else /** Assert that an autocommit non-locking select cannot be in the rw_trx_list and that it is a read-only transaction. The tranasction must be in the mysql_trx_list. */ @@ -511,10 +575,17 @@ transaction pool. check_trx_state(t); \ } \ } while (0) -#else /* UNIV_DEBUG */ +#endif /* HAVE_LOCK_FREE_READVIEW */ +#else /* UNIV_DEBUG */ +#ifdef HAVE_LOCK_FREE_READVIEW +/** Assert that an autocommit non-locking select cannot be in the + rw_trx_hash and that it is a read-only transaction. + The tranasction must have mysql_thd assigned. */ +#else /** Assert that an autocommit non-locking slect cannot be in the rw_trx_list and that it is a read-only transaction. The tranasction must be in the mysql_trx_list. */ +#endif /* HAVE_LOCK_FREE_READVIEW */ #define assert_trx_nonlocking_or_in_list(trx) ((void)0) #endif /* UNIV_DEBUG */ #endif /* !UNIV_HOTBACKUP */ @@ -778,6 +849,18 @@ enum trx_rseg_type_t { }; struct trx_t { +#ifdef HAVE_LOCK_FREE_READVIEW + private: + std::atomic n_ref; /*!< Count of references, protected + by trx_t::mutex. We can't release the + locks nor commit the transaction until + this reference is 0. We can change + the state to COMMITTED_IN_MEMORY to + signify that it is no longer + "active". */ + + public: +#endif /* HAVE_LOCK_FREE_READVIEW */ enum isolation_level_t { /** dirty read: non-locking SELECTs are performed so that we @@ -828,6 +911,15 @@ struct trx_t { trx_id_t id; /*!< transaction id */ +#ifdef HAVE_LOCK_FREE_READVIEW + trx_id_t no; /*!< transaction serialization number: + max trx id shortly before the + transaction is moved to + COMMITTED_IN_MEMORY state. + Protected by trx_sys_t::mutex + when trx is in rw_trx_hash. Initially + set to TRX_ID_MAX. */ +#else trx_id_t no; /*!< transaction serialization number: max trx id shortly before the transaction is moved to @@ -835,7 +927,73 @@ struct trx_t { Protected by trx_sys_t::mutex when trx->in_rw_trx_list. Initially set to TRX_ID_MAX. */ +#endif /* HAVE_LOCK_FREE_READVIEW */ + +#ifdef HAVE_LOCK_FREE_READVIEW + /** State of the trx from the point of view of concurrency control + and the valid state transitions. + + Possible states: + + TRX_STATE_NOT_STARTED + TRX_STATE_FORCED_ROLLBACK + TRX_STATE_ACTIVE + TRX_STATE_PREPARED + TRX_STATE_COMMITTED_IN_MEMORY (alias below COMMITTED) + + Valid state transitions are: + + Regular transactions: + * NOT_STARTED -> ACTIVE -> COMMITTED -> NOT_STARTED + + Auto-commit non-locking read-only: + * NOT_STARTED -> ACTIVE -> NOT_STARTED + + XA (2PC): + * NOT_STARTED -> ACTIVE -> PREPARED -> COMMITTED -> NOT_STARTED + + Recovered XA: + * NOT_STARTED -> PREPARED -> COMMITTED -> (freed) + + XA (2PC) (shutdown or disconnect before ROLLBACK or COMMIT): + * NOT_STARTED -> PREPARED -> (freed) + + Disconnected XA can become recovered: + * ... -> ACTIVE -> PREPARED (connected) -> PREPARED (disconnected) + Disconnected means from mysql e.g due to the mysql client disconnection. + Latching and various transaction lists membership rules: + + XA (2PC) transactions are always treated as non-autocommit. + + Transitions to ACTIVE or NOT_STARTED occur when transaction + is not in rw_trx_hash (no trx_sys->mutex needed). + + Autocommit non-locking read-only transactions move between states + without holding any mutex. They are not in rw_trx_hash. + + All transactions, unless they are determined to be ac-nl-ro, + explicitly tagged as read-only or read-write, will first be put + on the read-only transaction list. Only when a !read-only transaction + in the read-only list tries to acquire an X or IX lock on a table + do we remove it from the read-only list and put it on the read-write + list. During this switch we assign it a rollback segment. + When a transaction is NOT_STARTED, it can be in trx_list. It cannot be + in rw_trx_hash. + + ACTIVE->PREPARED->COMMITTED is only possible when trx is in rw_trx_hash. + The transition ACTIVE->PREPARED is protected by trx_sys->mutex. + + ACTIVE->COMMITTED is possible when the transaction is in + rw_trx_hash. + + Transitions to COMMITTED are protected by both lock_sys->mutex + and trx->mutex. + + NOTE: Some of these state change constraints are an overkill, + currently only required for a consistent view for printing stats. + This unnecessarily adds a huge cost for the general case. */ +#else /** State of the trx from the point of view of concurrency control and the valid state transitions. @@ -898,6 +1056,7 @@ struct trx_t { NOTE: Some of these state change constraints are an overkill, currently only required for a consistent view for printing stats. This unnecessarily adds a huge cost for the general case. */ +#endif /* HAVE_LOCK_FREE_READVIEW */ trx_state_t state; @@ -908,6 +1067,14 @@ struct trx_t { concurrent unique insert or replace operation. */ bool skip_lock_inheritance; +#ifdef HAVE_LOCK_FREE_READVIEW + ReadView read_view; /*!< consistent read view used in the + transaction, or NULL if not yet set */ + + UT_LIST_NODE_T(trx_t) + trx_list; /*!< list of all transactions; + protected by trx_sys->mutex. */ +#else ReadView *read_view; /*!< consistent read view used in the transaction, or NULL if not yet set */ @@ -918,15 +1085,23 @@ struct trx_t { no_list; /*!< Required during view creation to check for the view limit for transactions that are committing */ +#endif /* HAVE_LOCK_FREE_READVIEW */ trx_lock_t lock; /*!< Information about the transaction locks and state. Protected by trx->mutex or lock_sys->mutex or both */ +#ifdef HAVE_LOCK_FREE_READVIEW + bool is_recovered; /*!< 0=normal transaction, + 1=recovered, must be rolled back, + protected by trx_sys->mutex when + trx is in rw_trx_hash */ +#else bool is_recovered; /*!< 0=normal transaction, 1=recovered, must be rolled back, protected by trx_sys->mutex when trx->in_rw_trx_list holds */ +#endif /* HAVE_LOCK_FREE_READVIEW */ os_thread_id_t killed_by; /*!< The thread ID that wants to kill this transaction asynchronously. @@ -1045,6 +1220,7 @@ struct trx_t { statement uses, except those in consistent read */ /*------------------------------*/ +#ifndef HAVE_LOCK_FREE_READVIEW #ifdef UNIV_DEBUG /** The following two fields are mutually exclusive. */ /* @{ */ @@ -1060,6 +1236,7 @@ struct trx_t { /*!< true if in trx_sys->mysql_trx_list */ #endif /* UNIV_DEBUG */ +#endif /* HAVE_LOCK_FREE_READVIEW */ /*------------------------------*/ dberr_t error_state; /*!< 0 if no error, otherwise error number; NOTE That ONLY the thread @@ -1155,6 +1332,7 @@ struct trx_t { const char *start_file; /*!< Filename where it was started */ #endif /* UNIV_DEBUG */ +#ifndef HAVE_LOCK_FREE_READVIEW lint n_ref; /*!< Count of references, protected by trx_t::mutex. We can't release the locks nor commit the transaction until @@ -1162,6 +1340,7 @@ struct trx_t { the state to COMMITTED_IN_MEMORY to signify that it is no longer "active". */ +#endif /* HAVE_LOCK_FREE_READVIEW */ /** Version of this instance. It is incremented each time the instance is re-used in trx_start_low(). It is used to track @@ -1190,6 +1369,10 @@ struct trx_t { doing Non-locking Read-only Read Committed on DD tables */ #endif /* UNIV_DEBUG */ +#ifdef HAVE_LOCK_FREE_READVIEW + rw_trx_hash_element_t *rw_trx_hash_element{nullptr}; + LF_PINS *rw_trx_hash_pins{nullptr}; +#endif /* HAVE_LOCK_FREE_READVIEW */ ulint magic_n; bool is_read_uncommitted() const { @@ -1210,6 +1393,20 @@ struct trx_t { } bool allow_semi_consistent() const { return (skip_gap_locks()); } + +#ifdef HAVE_LOCK_FREE_READVIEW + bool is_referenced() { return (n_ref.load(std::memory_order_relaxed) > 0); } + + void reference() { + int32_t old_n_ref = n_ref.fetch_add(1, std::memory_order_relaxed); + ut_a(old_n_ref >= 0); + } + + void release_reference() { + int32_t old_n_ref = n_ref.fetch_sub(1, std::memory_order_relaxed); + ut_a(old_n_ref > 0); + } +#endif /* HAVE_LOCK_FREE_READVIEW */ }; #ifndef UNIV_HOTBACKUP diff --git a/storage/innobase/include/trx0trx.ic b/storage/innobase/include/trx0trx.ic index f33f2d5..cf4bb2a 100644 --- a/storage/innobase/include/trx0trx.ic +++ b/storage/innobase/include/trx0trx.ic @@ -32,11 +32,19 @@ this program; if not, write to the Free Software Foundation, Inc., #include "read0read.h" +#ifdef HAVE_LOCK_FREE_READVIEW +/** Determines if a transaction is in the given state. + The caller must hold trx_sys->mutex, or it must be the thread + that is serving a running transaction. + A running RW transaction must be in trx_sys->rw_trx_hash. + @return true if trx->state == state */ +#else /** Determines if a transaction is in the given state. The caller must hold trx_sys->mutex, or it must be the thread that is serving a running transaction. A running RW transaction must be in trx_sys->rw_trx_list. @return true if trx->state == state */ +#endif /* HAVE_LOCK_FREE_READVIEW */ UNIV_INLINE bool trx_state_eq(const trx_t *trx, /*!< in: transaction */ trx_state_t state) /*!< in: state */ @@ -65,7 +73,9 @@ bool trx_state_eq(const trx_t *trx, /*!< in: transaction */ ut_a(state == TRX_STATE_NOT_STARTED || state == TRX_STATE_FORCED_ROLLBACK); +#ifndef HAVE_LOCK_FREE_READVIEW ut_ad(!trx->in_rw_trx_list); +#endif /* HAVE_LOCK_FREE_READVIEW */ return (true); } @@ -216,6 +226,7 @@ bool trx_is_rseg_assigned(const trx_t *trx) /*!< in: transaction */ return (trx->rsegs.m_redo.rseg != NULL || trx->rsegs.m_noredo.rseg != NULL); } +#ifndef HAVE_LOCK_FREE_READVIEW /** Increase the reference count. If the transaction is in state TRX_STATE_COMMITTED_IN_MEMORY then the transaction is considered @@ -253,13 +264,18 @@ void trx_release_reference(trx_t *trx) { trx_mutex_exit(trx); } +#endif /* HAVE_LOCK_FREE_READVIEW */ /** @param trx Get the active view for this transaction, if one exists @return the transaction's read view or NULL if one not assigned. */ UNIV_INLINE ReadView *trx_get_read_view(trx_t *trx) { +#ifdef HAVE_LOCK_FREE_READVIEW + return (!trx->read_view.is_open() ? nullptr : &trx->read_view); +#else return (!MVCC::is_view_active(trx->read_view) ? NULL : trx->read_view); +#endif /* HAVE_LOCK_FREE_READVIEW */ } /** @@ -267,7 +283,11 @@ ReadView *trx_get_read_view(trx_t *trx) { @return the transaction's read view or NULL if one not assigned. */ UNIV_INLINE const ReadView *trx_get_read_view(const trx_t *trx) { +#ifdef HAVE_LOCK_FREE_READVIEW + return (!trx->read_view.is_open() ? nullptr : &trx->read_view); +#else return (!MVCC::is_view_active(trx->read_view) ? NULL : trx->read_view); +#endif /* HAVE_LOCK_FREE_READVIEW */ } /** diff --git a/storage/innobase/include/trx0types.h b/storage/innobase/include/trx0types.h index a8f995b..08258c9 100644 --- a/storage/innobase/include/trx0types.h +++ b/storage/innobase/include/trx0types.h @@ -537,6 +537,7 @@ typedef std::priority_queue< typedef std::vector> trx_ids_t; +#ifndef HAVE_LOCK_FREE_READVIEW /** Mapping read-write transactions from id to transaction instance, for creating read views and during trx id lookup for MVCC and locking. */ struct TrxTrack { @@ -570,6 +571,7 @@ struct TrxTrackCmp { // typedef std::unordered_set TrxIdSet; typedef std::set> TrxIdSet; +#endif /* HAVE_LOCK_FREE_READVIEW */ struct TrxVersion { TrxVersion(trx_t *trx); diff --git a/storage/innobase/include/trx0undo.h b/storage/innobase/include/trx0undo.h index ad42905..345d813 100644 --- a/storage/innobase/include/trx0undo.h +++ b/storage/innobase/include/trx0undo.h @@ -272,9 +272,14 @@ the data can be discarded. @param[in] noredo whether the undo tablespace is redo logged */ void trx_undo_insert_cleanup(trx_undo_ptr_t *undo_ptr, bool noredo); +#ifdef HAVE_LOCK_FREE_READVIEW +/** At shutdown, frees the undo logs of a transaction. */ +void trx_undo_free_at_shutdown(trx_t *trx); +#else /** At shutdown, frees the undo logs of a PREPARED transaction. */ void trx_undo_free_prepared(trx_t *trx) /*!< in/out: PREPARED transaction */ UNIV_COLD; +#endif /* HAVE_LOCK_FREE_READVIEW */ /* Forward declaration. */ namespace undo { diff --git a/storage/innobase/include/ut0counter.h b/storage/innobase/include/ut0counter.h index 80db26f..0abec07 100644 --- a/storage/innobase/include/ut0counter.h +++ b/storage/innobase/include/ut0counter.h @@ -46,7 +46,7 @@ this program; if not, write to the Free Software Foundation, Inc., #include /** CPU cache line size */ -#ifdef __powerpc__ +#if (defined(__powerpc__)) || (defined(__aarch64__)) #define INNOBASE_CACHE_LINE_SIZE 128 #else #define INNOBASE_CACHE_LINE_SIZE 64 diff --git a/storage/innobase/include/ut0new.h b/storage/innobase/include/ut0new.h index 8d10abf..8085d64 100644 --- a/storage/innobase/include/ut0new.h +++ b/storage/innobase/include/ut0new.h @@ -182,7 +182,9 @@ extern PSI_memory_key mem_key_partitioning; extern PSI_memory_key mem_key_row_log_buf; extern PSI_memory_key mem_key_row_merge_sort; extern PSI_memory_key mem_key_std; +#ifndef HAVE_LOCK_FREE_READVIEW extern PSI_memory_key mem_key_trx_sys_t_rw_trx_ids; +#endif /* HAVE_LOCK_FREE_READVIEW */ extern PSI_memory_key mem_key_undo_spaces; extern PSI_memory_key mem_key_ut_lock_free_hash_t; /* Please obey alphabetical order in the definitions above. */ diff --git a/storage/innobase/lock/lock0lock.cc b/storage/innobase/lock/lock0lock.cc index 5e6cfe3..d1cdfd4 100644 --- a/storage/innobase/lock/lock0lock.cc +++ b/storage/innobase/lock/lock0lock.cc @@ -206,7 +206,11 @@ bool lock_check_trx_id_sanity( { ut_ad(rec_offs_validate(rec, index, offsets)); +#ifdef HAVE_LOCK_FREE_READVIEW + trx_id_t max_trx_id = trx_sys->get_max_trx_id(); +#else trx_id_t max_trx_id = trx_sys_get_max_trx_id(); +#endif /* HAVE_LOCK_FREE_READVIEW */ bool is_ok = trx_id < max_trx_id; if (!is_ok) { @@ -817,6 +821,9 @@ static const lock_t *lock_rec_other_has_conflicting( negatives. The caller must confirm all positive results by calling trx_is_active(). */ static trx_t *lock_sec_rec_some_has_impl( +#ifdef HAVE_LOCK_FREE_READVIEW + trx_t *caller_trx, /*!< in/out: trx of current thread */ +#endif /* HAVE_LOCK_FREE_READVIEW */ const rec_t *rec, /*!< in: user record */ dict_index_t *index, /*!< in: secondary index */ const ulint *offsets) /*!< in: rec_get_offsets(rec, index) */ @@ -826,7 +833,9 @@ static trx_t *lock_sec_rec_some_has_impl( const page_t *page = page_align(rec); ut_ad(!lock_mutex_own()); +#ifndef HAVE_LOCK_FREE_READVIEW ut_ad(!trx_sys_mutex_own()); +#endif /* HAVE_LOCK_FREE_READVIEW */ ut_ad(!index->is_clustered()); ut_ad(page_rec_is_user_rec(rec)); ut_ad(rec_offs_validate(rec, index, offsets)); @@ -839,6 +848,21 @@ static trx_t *lock_sec_rec_some_has_impl( max trx id to the log, and therefore during recovery, this value for a page may be incorrect. */ +#ifdef HAVE_LOCK_FREE_READVIEW + if (max_trx_id < trx_sys->get_min_trx_id()) { + trx = nullptr; + + } else if (!lock_check_trx_id_sanity(max_trx_id, rec, index, offsets)) { + /* The page is corrupt: try to avoid a crash by returning 0 */ + trx = nullptr; + + /* In this case it is possible that some transaction has an implicit + x-lock. We have to look in the clustered index. */ + + } else { + trx = row_vers_impl_x_locked(caller_trx, rec, index, offsets); + } +#else if (max_trx_id < trx_rw_min_trx_id() && !recv_recovery_is_on()) { trx = 0; @@ -852,11 +876,60 @@ static trx_t *lock_sec_rec_some_has_impl( } else { trx = row_vers_impl_x_locked(rec, index, offsets); } +#endif /* HAVE_LOCK_FREE_READVIEW */ return (trx); } #ifdef UNIV_DEBUG +#ifdef HAVE_LOCK_FREE_READVIEW +struct lock_rec_other_trx_holds_expl_arg { + ulint precise_mode; + const buf_block_t *const block; + ulint heap_no; + const trx_t *impl_trx; +}; + +static bool lock_rec_other_trx_holds_expl_callback( + rw_trx_hash_element_t *element, lock_rec_other_trx_holds_expl_arg *arg) { + mutex_enter(&element->mutex); + trx_t *trx = element->trx; + if (trx != nullptr) { + const lock_t *expl_lock = lock_rec_has_expl(arg->precise_mode, arg->block, + arg->heap_no, arg->impl_trx); + + ut_ad(!expl_lock || expl_lock->trx == arg->impl_trx); + } + mutex_exit(&element->mutex); + return false; +} + +/** Checks if some transaction, other than given trx_id, has an explicit + lock on the given rec, in the given precise_mode. */ +static void lock_rec_other_trx_holds_expl( + trx_t *caller_trx, /*!< in: current transaction */ + ulint precise_mode, /*!< in: LOCK_S or LOCK_X + possibly ORed to LOCK_GAP or + LOCK_REC_NOT_GAP. */ + trx_t *trx, /*!< in: trx holding implicit + lock on rec */ + const rec_t *rec, /*!< in: user record */ + const buf_block_t *block) /*!< in: buffer block + containing the record */ +{ + lock_mutex_enter(); + + ulint heap_no = page_rec_get_heap_no(rec); + lock_rec_other_trx_holds_expl_arg arg = {precise_mode, block, heap_no, trx}; + + trx_sys->rw_trx_hash.iterate(caller_trx, + reinterpret_cast( + lock_rec_other_trx_holds_expl_callback), + &arg); + + lock_mutex_exit(); +} +#else /** Checks if some transaction, other than given trx_id, has an explicit lock on the given rec, in the given precise_mode. @param[in] precise_mode LOCK_S or LOCK_X possibly ORed to LOCK_GAP or @@ -904,6 +977,7 @@ static bool lock_rec_other_trx_holds_expl(ulint precise_mode, const trx_t *trx, return (holds); } +#endif /* HAVE_LOCK_FREE_READVIEW */ #endif /* UNIV_DEBUG */ /** Return approximate number or record locks (bits set in the bitmap) for @@ -4382,6 +4456,83 @@ static void lock_remove_all_on_table_for_trx( trx_mutex_exit(trx); } +#ifdef HAVE_LOCK_FREE_READVIEW +struct lock_remove_recovered_trx_record_locks_arg { + dict_table_t *table; + ulint n_recovered_trx; +}; + +static bool lock_remove_recovered_trx_record_locks_callback( + rw_trx_hash_element_t *element, + lock_remove_recovered_trx_record_locks_arg *arg) { + mutex_enter(&element->mutex); + trx_t *trx = element->trx; + if (trx != nullptr) { + if (!trx->is_recovered) { + mutex_exit(&element->mutex); + return false; + } + + /* Because we are holding the lock_sys->mutex, + implicit locks cannot be converted to explicit ones + while we are scanning the explicit locks. */ + + for (lock_t *lock = UT_LIST_GET_FIRST(trx->lock.trx_locks); lock != nullptr; + lock = UT_LIST_GET_NEXT(trx_locks, lock)) { + ut_a(lock->trx == trx); + + /* Recovered transactions can't wait on a lock. */ + +#ifdef HAVE_SCALABLE_LOCK_MGR + ut_a(!lock->is_waiting()); +#else + ut_a(!lock_get_wait(lock)); +#endif /* HAVE_SCALABLE_LOCK_MGR */ + + switch (lock_get_type_low(lock)) { + default: + ut_error; + case LOCK_TABLE: + if (lock->tab_lock.table == arg->table) { + lock_trx_table_locks_remove(lock); + lock_table_remove_low(lock); + } + break; + case LOCK_REC: + if (lock->index->table == arg->table) { + lock_rec_discard(lock); + } + } + } + + ++arg->n_recovered_trx; + } + mutex_exit(&element->mutex); + return false; +} + +/** Remove any explicit record locks held by recovering transactions on + the table. + @return number of recovered transactions examined */ +static ulint lock_remove_recovered_trx_record_locks( + dict_table_t *table) /*!< in: check if there are any locks + held on records in this table or on the + table itself */ +{ + ut_a(table != nullptr); + ut_ad(lock_mutex_own()); + + lock_remove_recovered_trx_record_locks_arg arg = {table, 0}; + + trx_sys->rw_trx_hash.iterate( + reinterpret_cast( + lock_remove_recovered_trx_record_locks_callback), + &arg); + + return (arg.n_recovered_trx); +} +#else + /** Remove any explicit record locks held by recovering transactions on the table. @return number of recovered transactions examined */ @@ -4452,6 +4603,7 @@ static ulint lock_remove_recovered_trx_record_locks( return (n_recovered_trx); } +#endif /* HAVE_LOCK_FREE_READVIEW */ /** Removes locks on a table to be dropped. If remove_also_table_sx_locks is true then table-level S and X locks are @@ -4709,7 +4861,11 @@ bool lock_print_info_summary( "------------\n", file); +#ifdef HAVE_LOCK_FREE_READVIEW + fprintf(file, "Trx id counter " TRX_ID_FMT "\n", trx_sys->get_max_trx_id()); +#else fprintf(file, "Trx id counter " TRX_ID_FMT "\n", trx_sys_get_max_trx_id()); +#endif /* HAVE_LOCK_FREE_READVIEW */ fprintf(file, "Purge done for trx's n:o < " TRX_ID_FMT " undo n:o < " TRX_ID_FMT @@ -4766,7 +4922,9 @@ struct PrintNotStarted { void operator()(const trx_t *trx) { /* We require exclusive access to lock_sys */ ut_ad(lock_mutex_own()); +#ifndef HAVE_LOCK_FREE_READVIEW ut_ad(trx->in_mysql_trx_list); +#endif /* HAVE_LOCK_FREE_READVIEW */ ut_ad(mutex_own(&trx_sys->mutex)); /* See state transitions and locking rules in trx0trx.h */ @@ -4818,6 +4976,7 @@ class TrxLockIterator { ulint m_index; }; +#ifndef HAVE_LOCK_FREE_READVIEW /** This iterates over both the RW and RO trx_sys lists. We need to keep track where the iterator was up to and we do that using an ordinal value. */ @@ -4875,6 +5034,7 @@ class TrxListIterator { /** For iterating over a transaction's locks */ TrxLockIterator m_lock_iter; }; +#endif /* HAVE_LOCK_FREE_READVIEW */ /** Prints transaction lock wait and MVCC state. @param[in,out] file file where to print @@ -4887,11 +5047,22 @@ void lock_trx_print_wait_and_mvcc_state(FILE *file, const trx_t *trx) { trx_print_latched(file, trx, 600); +#ifdef HAVE_LOCK_FREE_READVIEW + /* Note: read_view->get_state() check is race condition. But it + should "kind of work" because read_view is freed only at shutdown. + Worst thing that may happen is that it'll get transferred to + another thread and print wrong values. */ + + if (READ_VIEW_STATE_OPEN == trx->read_view.get_state()) { + trx->read_view.print_limits(file); + } +#else const ReadView *read_view = trx_get_read_view(trx); if (read_view != NULL) { read_view->print_limits(file); } +#endif /* HAVE_LOCK_FREE_READVIEW */ if (trx->lock.que_state == TRX_QUE_LOCK_WAIT) { fprintf(file, @@ -4909,6 +5080,65 @@ void lock_trx_print_wait_and_mvcc_state(FILE *file, const trx_t *trx) { } } +#ifdef HAVE_LOCK_FREE_READVIEW +/** Prints info of locks for a transaction. + @return true if all printed, false if latches were released. */ +static void lock_trx_print_locks( + FILE *file, /*!< in/out: File to write */ + const trx_t *trx) /*!< in: current transaction */ +{ + const lock_t *lock; + uint32_t i = 0; + + /* Iterate over the transaction's locks. */ + for (lock = UT_LIST_GET_FIRST(trx->lock.trx_locks); lock != nullptr; + lock = UT_LIST_GET_NEXT(trx_locks, lock)) { + if (lock_get_type_low(lock) == LOCK_REC) { + /* Print all the record locks on the page from + the record lock bitmap */ + + lock_rec_print(file, lock); + } else { + ut_ad(lock_get_type_low(lock) & LOCK_TABLE); + + lock_table_print(file, lock); + } + + if (++i == 10) { + fprintf(file, + "10 LOCKS PRINTED FOR THIS TRX:" + " SUPPRESSING FURTHER PRINTS\n"); + + break; + } + } +} + +static bool lock_print_info_all_transactions_callback( + rw_trx_hash_element_t *element, FILE *file) { + mutex_enter(&element->mutex); + trx_t *trx = element->trx; + if (trx != nullptr) { + check_trx_state(trx); + + lock_trx_print_wait_and_mvcc_state(file, trx); + + /* If we need to print the locked record contents then we + need to fetch the containing block from the buffer pool. */ + if (srv_print_innodb_lock_monitor) { + /* Print the locks owned by the current transaction. */ + + trx->reference(); + mutex_exit(&element->mutex); + lock_trx_print_locks(file, trx); + trx->release_reference(); + return false; + } + } + mutex_exit(&element->mutex); + return false; +} +#else /** Prints info of locks for a transaction. This function will release the lock mutex and the trx_sys_t::mutex if the page was read from disk. @return true if page was read from the tablespace */ @@ -5019,6 +5249,7 @@ static bool lock_trx_print_locks( return (true); } +#endif /* HAVE_LOCK_FREE_READVIEW */ /** Prints info of locks for each transaction. This function assumes that the caller holds the lock mutex and more importantly it will release the lock @@ -5031,7 +5262,9 @@ void lock_print_info_all_transactions( fprintf(file, "LIST OF TRANSACTIONS FOR EACH SESSION:\n"); +#ifndef HAVE_LOCK_FREE_READVIEW mutex_enter(&trx_sys->mutex); +#endif /* HAVE_LOCK_FREE_READVIEW */ /* First print info on non-active transactions */ @@ -5040,6 +5273,15 @@ void lock_print_info_all_transactions( available from INFORMATION_SCHEMA.INNODB_TRX. */ PrintNotStarted print_not_started(file); +#ifdef HAVE_LOCK_FREE_READVIEW + mutex_enter(&trx_sys->mutex); + ut_list_map(trx_sys->trx_list, print_not_started); + mutex_exit(&trx_sys->mutex); + trx_sys->rw_trx_hash.iterate( + reinterpret_cast( + lock_print_info_all_transactions_callback), + file); +#else ut_list_map(trx_sys->mysql_trx_list, print_not_started); const trx_t *trx; @@ -5089,9 +5331,12 @@ void lock_print_info_all_transactions( a page from disk, or we didn't need to print the detail. */ trx_iter.next(); } +#endif /* HAVE_LOCK_FREE_READVIEW */ lock_mutex_exit(); +#ifndef HAVE_LOCK_FREE_READVIEW mutex_exit(&trx_sys->mutex); +#endif /* HAVE_LOCK_FREE_READVIEW */ ut_ad(lock_validate()); } @@ -5125,7 +5370,9 @@ static bool lock_table_queue_validate( const lock_t *lock; ut_ad(lock_mutex_own()); +#ifndef HAVE_LOCK_FREE_READVIEW ut_ad(trx_sys_mutex_own()); +#endif /* HAVE_LOCK_FREE_READVIEW */ for (lock = UT_LIST_GET_FIRST(table->locks); lock != NULL; lock = UT_LIST_GET_NEXT(tab_lock.locks, lock)) { @@ -5209,14 +5456,22 @@ static bool lock_rec_queue_validate( trx_id = lock_clust_rec_some_has_impl(rec, index, offsets); +#ifdef HAVE_LOCK_FREE_READVIEW + const trx_t *impl_trx = nullptr; + impl_trx = + (trx_id > 0) ? trx_sys->find(current_trx(), trx_id, false) : nullptr; +#else const trx_t *impl_trx = trx_rw_is_active_low(trx_id, NULL); +#endif /* HAVE_LOCK_FREE_READVIEW */ if (impl_trx != nullptr) { ut_ad(lock_mutex_own()); +#ifndef HAVE_LOCK_FREE_READVIEW ut_ad(trx_sys_mutex_own()); /* impl_trx cannot become TRX_STATE_COMMITTED_IN_MEMORY nor removed from rw_trx_set until we release trx_sys->mutex, which means that currently all other threads in the system consider this impl_trx active and thus should respect implicit locks held by impl_trx*/ +#endif /* HAVE_LOCK_FREE_READVIEW */ const lock_t *other_lock = lock_rec_other_has_expl_req(LOCK_S, block, true, heap_no, impl_trx); @@ -5347,6 +5602,38 @@ function_exit: return (true); } +#ifdef HAVE_LOCK_FREE_READVIEW +static bool lock_validate_table_locks_callback(rw_trx_hash_element_t *element, + void * /*unused*/) { + mutex_enter(&element->mutex); + trx_t *trx = element->trx; + if (trx != nullptr) { + const lock_t *lock = nullptr; + + check_trx_state(trx); + + for (lock = UT_LIST_GET_FIRST(trx->lock.trx_locks); lock != nullptr; + lock = UT_LIST_GET_NEXT(trx_locks, lock)) { + if ((lock_get_type_low(lock) & LOCK_TABLE) != 0u) { + lock_table_queue_validate(lock->tab_lock.table); + } + } + } + mutex_exit(&element->mutex); + return false; +} + +/** Validates the table locks. + @return true if ok */ +static bool lock_validate_table_locks() { + ut_ad(lock_mutex_own()); + trx_sys->rw_trx_hash.iterate( + reinterpret_cast(lock_validate_table_locks_callback), + nullptr); + + return (true); +} +#else /** Validates the table locks. @return true if ok */ static bool lock_validate_table_locks( @@ -5376,6 +5663,7 @@ static bool lock_validate_table_locks( return (true); } +#endif /* HAVE_LOCK_FREE_READVIEW */ /** Validate record locks up to a limit. @return lock at limit or NULL if no more locks in the hash bucket */ @@ -5386,7 +5674,9 @@ static MY_ATTRIBUTE((warn_unused_result)) const lock_t *lock_rec_validate( (space, page_no) */ { ut_ad(lock_mutex_own()); +#ifndef HAVE_LOCK_FREE_READVIEW ut_ad(trx_sys_mutex_own()); +#endif /* HAVE_LOCK_FREE_READVIEW */ for (const lock_t *lock = static_cast( HASH_GET_FIRST(lock_sys->rec_hash, start)); @@ -5447,9 +5737,13 @@ static bool lock_validate() { page_addr_set pages; lock_mutex_enter(); +#ifdef HAVE_LOCK_FREE_READVIEW + ut_a(lock_validate_table_locks()); +#else mutex_enter(&trx_sys->mutex); ut_a(lock_validate_table_locks(&trx_sys->rw_trx_list)); +#endif /* HAVE_LOCK_FREE_READVIEW */ /* Iterate over all the record locks and validate the locks. We don't want to hog the lock_sys_t::mutex and the trx_sys_t::mutex. @@ -5469,7 +5763,9 @@ static bool lock_validate() { } } +#ifndef HAVE_LOCK_FREE_READVIEW mutex_exit(&trx_sys->mutex); +#endif /* HAVE_LOCK_FREE_READVIEW */ lock_mutex_exit(); for (page_addr_set::const_iterator it = pages.begin(); it != pages.end(); @@ -5635,7 +5931,11 @@ static void lock_rec_convert_impl_to_expl_for_trx( trx_t *trx, /*!< in/out: active transaction */ ulint heap_no) /*!< in: rec heap number to lock */ { +#ifdef HAVE_LOCK_FREE_READVIEW + ut_ad(trx->is_referenced()); +#else ut_ad(trx_is_referenced(trx)); +#endif /* HAVE_LOCK_FREE_READVIEW */ DEBUG_SYNC_C("before_lock_rec_convert_impl_to_expl_for_trx"); @@ -5688,11 +5988,80 @@ static void lock_rec_convert_impl_to_expl_for_trx( lock_mutex_exit(); trx_mutex_exit(trx); +#ifdef HAVE_LOCK_FREE_READVIEW + trx->release_reference(); +#else trx_release_reference(trx); +#endif /* HAVE_LOCK_FREE_READVIEW */ DEBUG_SYNC_C("after_lock_rec_convert_impl_to_expl_for_trx"); } +#ifdef HAVE_LOCK_FREE_READVIEW +/** If a transaction has an implicit x-lock on a record, but no explicit x-lock +set on the record, sets one for it. +@param[in,out] caller_trx current transaction +@param[in] block buffer block of rec +@param[in] rec user record on page +@param[in] index index of record +@param[in] offsets rec_get_offsets(rec, index) +@return whether caller_trx already holds an exclusive lock on rec */ +static bool lock_rec_convert_impl_to_expl(trx_t *caller_trx, + const buf_block_t *block, + const rec_t *rec, dict_index_t *index, + const ulint *offsets) { + trx_t *trx; + + ut_ad(!lock_mutex_own()); + ut_ad(page_rec_is_user_rec(rec)); + ut_ad(rec_offs_validate(rec, index, offsets)); + ut_ad(!page_rec_is_comp(rec) == !rec_offs_comp(offsets)); + + if (index->is_clustered()) { + trx_id_t trx_id; + + trx_id = lock_clust_rec_some_has_impl(rec, index, offsets); + + if (trx_id == 0) { + return false; + } + if (UNIV_UNLIKELY(trx_id == caller_trx->id)) { + return true; + } + + trx = trx_sys->find(caller_trx, trx_id); + } else { + ut_ad(!dict_index_is_online_ddl(index)); + + trx = lock_sec_rec_some_has_impl(caller_trx, rec, index, offsets); + if (trx == caller_trx) { + trx->release_reference(); + return true; + } + + if (trx != nullptr) { + DEBUG_SYNC_C("lock_rec_convert_impl_to_expl_will_validate"); + ut_d(lock_rec_other_trx_holds_expl(caller_trx, LOCK_S | LOCK_REC_NOT_GAP, + trx, rec, block)); + } + } + + if (trx != nullptr) { + ulint heap_no = page_rec_get_heap_no(rec); + + ut_ad(trx->is_referenced()); + + /* If the transaction is still active and has no + explicit x-lock set on the record, set one for it. + trx cannot be committed until the ref count is zero. */ + + lock_rec_convert_impl_to_expl_for_trx(block, rec, index, offsets, trx, + heap_no); + } + + return false; +} +#else /** If a transaction has an implicit x-lock on a record, but no explicit x-lock set on the record, sets one for it. @param[in] block buffer block of rec @@ -5741,12 +6110,17 @@ static void lock_rec_convert_impl_to_expl(const buf_block_t *block, heap_no); } } +#endif /* HAVE_LOCK_FREE_READVIEW */ void lock_rec_convert_active_impl_to_expl(const buf_block_t *block, const rec_t *rec, dict_index_t *index, const ulint *offsets, trx_t *trx, ulint heap_no) { +#ifdef HAVE_LOCK_FREE_READVIEW + trx->reference(); +#else trx_reference(trx, true); +#endif /* HAVE_LOCK_FREE_READVIEW */ lock_rec_convert_impl_to_expl_for_trx(block, rec, index, offsets, trx, heap_no); } @@ -5786,7 +6160,15 @@ dberr_t lock_clust_rec_modify_check_and_lock( /* If a transaction has no explicit x-lock set on the record, set one for it */ +#ifdef HAVE_LOCK_FREE_READVIEW + if (lock_rec_convert_impl_to_expl(thr_get_trx(thr), block, rec, index, + offsets)) { + /* We already hold an implicit exclusive lock. */ + return DB_SUCCESS; + } +#else lock_rec_convert_impl_to_expl(block, rec, index, offsets); +#endif /* HAVE_LOCK_FREE_READVIEW */ lock_mutex_enter(); @@ -5909,11 +6291,23 @@ dberr_t lock_sec_rec_read_check_and_lock( if the max trx id for the page >= min trx id for the trx list or a database recovery is running. */ +#ifdef HAVE_LOCK_FREE_READVIEW + if ((page_get_max_trx_id(block->frame) >= trx_sys->get_min_trx_id() || + recv_recovery_is_on()) && + page_rec_is_supremum(rec) == 0u) { + if (lock_rec_convert_impl_to_expl(thr_get_trx(thr), block, rec, index, + offsets)) { + /* We already hold an implicit exclusive lock. */ + return DB_SUCCESS; + } + } +#else if ((page_get_max_trx_id(block->frame) >= trx_rw_min_trx_id() || recv_recovery_is_on()) && !page_rec_is_supremum(rec)) { lock_rec_convert_impl_to_expl(block, rec, index, offsets); } +#endif /* HAVE_LOCK_FREE_READVIEW */ lock_mutex_enter(); @@ -5962,7 +6356,15 @@ dberr_t lock_clust_rec_read_check_and_lock( heap_no = page_rec_get_heap_no(rec); if (heap_no != PAGE_HEAP_NO_SUPREMUM) { +#ifdef HAVE_LOCK_FREE_READVIEW + if (lock_rec_convert_impl_to_expl(thr_get_trx(thr), block, rec, index, + offsets)) { + /* We already hold an implicit exclusive lock. */ + return DB_SUCCESS; + } +#else lock_rec_convert_impl_to_expl(block, rec, index, offsets); +#endif /* HAVE_LOCK_FREE_READVIEW */ } DEBUG_SYNC_C("after_lock_clust_rec_read_check_and_lock_impl_to_expl"); @@ -6408,8 +6810,13 @@ void lock_trx_release_locks(trx_t *trx) /*!< in/out: transaction */ check_trx_state(trx); ut_ad(trx_state_eq(trx, TRX_STATE_COMMITTED_IN_MEMORY)); +#ifdef HAVE_LOCK_FREE_READVIEW + if (trx->is_referenced()) { + while (trx->is_referenced()) { +#else if (trx_is_referenced(trx)) { while (trx_is_referenced(trx)) { +#endif /* HAVE_LOCK_FREE_READVIEW */ trx_mutex_exit(trx); DEBUG_SYNC_C("waiting_trx_is_not_referenced"); @@ -6422,7 +6829,11 @@ void lock_trx_release_locks(trx_t *trx) /*!< in/out: transaction */ } } +#ifdef HAVE_LOCK_FREE_READVIEW + ut_ad(!trx->is_referenced()); +#else ut_ad(!trx_is_referenced(trx)); +#endif /* HAVE_LOCK_FREE_READVIEW */ /* If the background thread trx_rollback_or_clean_recovered() is still active then there is a chance that the rollback @@ -6496,6 +6907,61 @@ dberr_t lock_trx_handle_wait(trx_t *trx) /*!< in/out: trx lock state */ } #ifdef UNIV_DEBUG +#ifdef HAVE_LOCK_FREE_READVIEW +struct lock_table_locks_lookup_arg { + const dict_table_t *table; + const lock_t *lock; +}; + +static bool lock_table_locks_lookup_callback(rw_trx_hash_element_t *element, + lock_table_locks_lookup_arg *arg) { + mutex_enter(&element->mutex); + trx_t *trx = element->trx; + if (trx != nullptr) { + const lock_t *lock = nullptr; + + check_trx_state(trx); + + for (lock = UT_LIST_GET_FIRST(trx->lock.trx_locks); lock != nullptr; + lock = UT_LIST_GET_NEXT(trx_locks, lock)) { + ut_a(lock->trx == trx); + + if (lock_get_type_low(lock) == LOCK_REC) { + ut_ad(!dict_index_is_online_ddl(lock->index) || + lock->index->is_clustered()); + if (lock->index->table == arg->table) { + arg->lock = lock; + break; + } + } else if (lock->tab_lock.table == arg->table) { // NOLINT + arg->lock = lock; + break; + } + } + } + mutex_exit(&element->mutex); + return (arg->lock != nullptr); +} + +/** Do an exhaustive check for any locks (table or rec) against the table. + @return lock if found */ +static const lock_t *lock_table_locks_lookup( + const dict_table_t *table) /*!< in: check if there are + any locks held on records in + this table or on the table + itself */ +{ + ut_a(table != nullptr); + + lock_table_locks_lookup_arg arg = {table, nullptr}; + + trx_sys->rw_trx_hash.iterate( + reinterpret_cast(lock_table_locks_lookup_callback), + &arg); + + return (arg.lock); +} +#else /** Do an exhaustive check for any locks (table or rec) against the table. @return lock if found */ static const lock_t *lock_table_locks_lookup( @@ -6535,6 +7001,7 @@ static const lock_t *lock_table_locks_lookup( return (NULL); } +#endif /* HAVE_LOCK_FREE_READVIEW */ #endif /* UNIV_DEBUG */ /** Check if there are any locks (table or rec) against table. @@ -6552,11 +7019,15 @@ bool lock_table_has_locks( #ifdef UNIV_DEBUG if (!has_locks) { +#ifdef HAVE_LOCK_FREE_READVIEW + ut_ad(lock_table_locks_lookup(table) == nullptr); +#else mutex_enter(&trx_sys->mutex); ut_ad(!lock_table_locks_lookup(table, &trx_sys->rw_trx_list)); mutex_exit(&trx_sys->mutex); +#endif /* HAVE_LOCK_FREE_READVIEW */ } #endif /* UNIV_DEBUG */ diff --git a/storage/innobase/page/page0page.cc b/storage/innobase/page/page0page.cc index 70521c3..e429953 100644 --- a/storage/innobase/page/page0page.cc +++ b/storage/innobase/page/page0page.cc @@ -2180,7 +2180,11 @@ ibool page_validate( trx_id_t max_trx_id = page_get_max_trx_id(page); /* This will be 0 during recv_apply_hashed_log_recs(TRUE), because the transaction system has not been initialized yet */ +#ifdef HAVE_LOCK_FREE_READVIEW + trx_id_t sys_max_trx_id = trx_sys->get_max_trx_id(); +#else trx_id_t sys_max_trx_id = trx_sys_get_max_trx_id(); +#endif /* HAVE_LOCK_FREE_READVIEW */ if (max_trx_id == 0 || (sys_max_trx_id != 0 && max_trx_id > sys_max_trx_id)) { diff --git a/storage/innobase/read/read0read.cc b/storage/innobase/read/read0read.cc index f5b6ff9..c543929 100644 --- a/storage/innobase/read/read0read.cc +++ b/storage/innobase/read/read0read.cc @@ -182,6 +182,7 @@ will mark their views as closed but not actually free their views. /** Minimum number of elements to reserve in ReadView::ids_t */ static const ulint MIN_TRX_IDS = 32; +#ifndef HAVE_LOCK_FREE_READVIEW #ifdef UNIV_DEBUG /** Functor to validate the view list. */ struct ViewCheck { @@ -322,12 +323,24 @@ ReadView::ReadView() ut_d(m_view_low_limit_no = 0); } +#else +/** +ReadView constructor */ +ReadView::ReadView() + : m_low_limit_id(), + m_up_limit_id(), + m_creator_trx_id(), + m_low_limit_no(), + m_state(READ_VIEW_STATE_CLOSED) {} +#endif /* HAVE_LOCK_FREE_READVIEW */ + /** ReadView destructor */ ReadView::~ReadView() { // Do nothing } +#ifndef HAVE_LOCK_FREE_READVIEW /** Constructor @param size Number of views to pre-allocate */ MVCC::MVCC(ulint size) { @@ -745,3 +758,122 @@ void MVCC::set_view_creator_trx_id(ReadView *view, trx_id_t id) { view->creator_trx_id(id); } +#endif /* HAVE_LOCK_FREE_READVIEW */ + +#ifdef HAVE_LOCK_FREE_READVIEW +void ReadView::copy(const ReadView &other) { + ut_ad(&other != this); + if (m_low_limit_no > other.m_low_limit_no) { + m_low_limit_no = other.m_low_limit_no; + } + if (m_low_limit_id > other.m_low_limit_id) { + m_low_limit_id = other.m_low_limit_id; + } + + auto dst = m_ids.begin(); + for (auto id : other.m_ids) { + if (id >= m_low_limit_id) { + break; + } + + loop: + if (dst == m_ids.end()) { + m_ids.push_back(id); + dst = m_ids.end(); + continue; + } + if (*dst < id) { + dst++; + goto loop; + } else if (*dst > id) { + dst = m_ids.insert(dst, id) + 1; + } + } + m_ids.erase(std::lower_bound(dst, m_ids.end(), m_low_limit_id), m_ids.end()); + + m_up_limit_id = m_ids.empty() ? m_low_limit_id : m_ids.front(); + ut_ad(m_up_limit_id <= m_low_limit_id); +} + +/** Creates a snapshot where exactly the transaction serialized before this +point in time are seen in the view. + +@param[in,out] trx transaction */ +void ReadView::snapshot(trx_t *trx) { + trx_sys->snapshot_ids(trx, &m_ids, &m_low_limit_id, &m_low_limit_no); + std::sort(m_ids.begin(), m_ids.end()); + m_up_limit_id = m_ids.empty() ? m_low_limit_id : m_ids.front(); + ut_ad(m_up_limit_id <= m_low_limit_id); +} + +/** Open a read view where exactly the transactions serialized before this point +in time are seen in the view. + +View becomes visible to purge thread. + +@param[in,out] trx transaction*/ +void ReadView::open(trx_t *trx) { + ut_ad(this == &trx->read_view); + switch (m_state.load(std::memory_order_relaxed)) { + case READ_VIEW_STATE_OPEN: + ut_ad(!srv_read_only_mode); + return; + case READ_VIEW_STATE_CLOSED: + if (srv_read_only_mode) { + return; + } + /* Reuse closed view if there were no read-write transactions since (and + at) its creation time. + + Original comment states: there is an inherent race between purge and this + thread. + + To avoid this race we should've checked trx_sys->get_max_trx_id() and set + state to READ_VIEW_STATE_OPEN atomically under trx_sys->mutex protection. + But we'are cutting edges to achieve greate scalability. + + There's at least two types of concurrent threads interested in this value: + purge coordinator thread (sees trx_sys_t::clone_oldest_view()) and InnoDB + monitor thread (see lock_trx_print_wait_and_mvcc_state()). + + What bad things can happen because we allow this race? + + Speculative execution may reorder state change before get_max_trx_id(). In + this case purge thread has short gap to clone outdated view. Which is + probably not that bad: it just won't be able to purge things that it was + actually allowed to purge for a short while. + + This thread may as well get suspended after trx_sys->get_max_trx_id() and + before state is set to READ_VIEW_STATE_OPEN. New read-write transaction + may get started, committed and purged meanwhile. It is acceptable as well, + since this view doesn't see it. */ + if (trx_is_autocommit_non_locking(trx) && m_ids.empty() && + m_low_limit_id == trx_sys->get_max_trx_id()) { + goto reopen; + } + + /* Can't reuse view, take new snapshot. + + Alas this empty critical section is simplest way to make sure concurrent + purge thread completed snapshot copy. Of course purge thread may come + again and try to copy once again after we release this mutex, but in this + case it is guaranteed to see READ_VIEW_STATE_REGISTERED and thus it'll + skip this view. + + This critical section can be replaced with new state, which purge thread + would set to inform us to wait until it completes snapshot. However it'd + complicate m_state even further. */ + mutex_enter(&trx_sys->mutex); + mutex_exit(&trx_sys->mutex); + m_state.store(READ_VIEW_STATE_SNAPSHOT, std::memory_order_relaxed); + break; + default: + ut_ad(false); + } + + snapshot(trx); +reopen: + m_creator_trx_id = trx->id; + m_state.store(READ_VIEW_STATE_OPEN, std::memory_order_release); +} +#endif /* HAVE_LOCK_FREE_READVIEW */ diff --git a/storage/innobase/row/row0ins.cc b/storage/innobase/row/row0ins.cc index 20ac667..3a7c5c3 100644 --- a/storage/innobase/row/row0ins.cc +++ b/storage/innobase/row/row0ins.cc @@ -706,7 +706,9 @@ static void row_ins_foreign_trx_print(trx_t *trx) /*!< in: transaction */ heap_size = mem_heap_get_size(trx->lock.lock_heap); lock_mutex_exit(); +#ifndef HAVE_LOCK_FREE_READVIEW trx_sys_mutex_enter(); +#endif /* HAVE_LOCK_FREE_READVIEW */ mutex_enter(&dict_foreign_err_mutex); rewind(dict_foreign_err_file); @@ -716,7 +718,9 @@ static void row_ins_foreign_trx_print(trx_t *trx) /*!< in: transaction */ trx_print_low(dict_foreign_err_file, trx, 600, n_rec_locks, n_trx_locks, heap_size); +#ifndef HAVE_LOCK_FREE_READVIEW trx_sys_mutex_exit(); +#endif /* HAVE_LOCK_FREE_READVIEW */ ut_ad(mutex_own(&dict_foreign_err_mutex)); } diff --git a/storage/innobase/row/row0merge.cc b/storage/innobase/row/row0merge.cc index a4394b5..06507cd 100644 --- a/storage/innobase/row/row0merge.cc +++ b/storage/innobase/row/row0merge.cc @@ -1915,6 +1915,24 @@ static MY_ATTRIBUTE((warn_unused_result)) dberr_t ONLINE_INDEX_COMPLETE state between the time the DML thread has updated the clustered index but has not yet accessed secondary index. */ +#ifdef HAVE_LOCK_FREE_READVIEW + ut_ad(trx->read_view.is_open()); + + if (!trx->read_view.changes_visible( + row_get_rec_trx_id(rec, clust_index, offsets), old_table->name)) { + rec_t *old_vers; + + row_vers_build_for_consistent_read(rec, &mtr, clust_index, &offsets, + &trx->read_view, &row_heap, row_heap, + &old_vers, nullptr, nullptr); + + rec = old_vers; + + if (rec == nullptr) { + continue; + } + } +#else ut_ad(MVCC::is_view_active(trx->read_view)); if (!trx->read_view->changes_visible( @@ -1931,6 +1949,7 @@ static MY_ATTRIBUTE((warn_unused_result)) dberr_t continue; } } +#endif /* HAVE_LOCK_FREE_READVIEW */ if (rec_get_deleted_flag(rec, dict_table_is_comp(old_table))) { /* This record was deleted in the latest diff --git a/storage/innobase/row/row0mysql.cc b/storage/innobase/row/row0mysql.cc index 3188afb..a352401 100644 --- a/storage/innobase/row/row0mysql.cc +++ b/storage/innobase/row/row0mysql.cc @@ -4658,7 +4658,11 @@ dberr_t row_scan_index_for_mysql(row_prebuilt_t *prebuilt, dict_index_t *index, /* No INSERT INTO ... SELECT and non-locking selects only. */ trx_start_if_not_started_xa(prebuilt->trx, false); +#ifdef HAVE_LOCK_FREE_READVIEW + prebuilt->trx->read_view.open(prebuilt->trx); +#else trx_assign_read_view(prebuilt->trx); +#endif /* HAVE_LOCK_FREE_READVIEW */ auto trx = prebuilt->trx; diff --git a/storage/innobase/row/row0pread.cc b/storage/innobase/row/row0pread.cc index 3105cad..3660db8 100644 --- a/storage/innobase/row/row0pread.cc +++ b/storage/innobase/row/row0pread.cc @@ -334,10 +334,17 @@ bool Parallel_reader::Scan_ctx::check_visibility(const rec_t *&rec, mtr_t *mtr) { const auto table_name = m_config.m_index->table->name; +#ifndef HAVE_LOCK_FREE_READVIEW ut_ad(m_trx->read_view == nullptr || MVCC::is_view_active(m_trx->read_view)); +#endif /* HAVE_LOCK_FREE_READVIEW */ +#ifdef HAVE_LOCK_FREE_READVIEW + if (m_trx->read_view.is_open()) { + ReadView* view = const_cast(&m_trx->read_view); +#else if (m_trx->read_view != nullptr) { auto view = m_trx->read_view; +#endif /* HAVE_LOCK_FREE_READVIEW */ if (m_config.m_index->is_clustered()) { trx_id_t rec_trx_id; diff --git a/storage/innobase/row/row0row.cc b/storage/innobase/row/row0row.cc index edf01a4..3a7a4d6 100644 --- a/storage/innobase/row/row0row.cc +++ b/storage/innobase/row/row0row.cc @@ -376,6 +376,7 @@ static inline dtuple_t *row_build_low(ulint type, const dict_index_t *index, ut_ad(heap != NULL); ut_ad(index->is_clustered()); ut_ad(!trx_sys_mutex_own()); + ut_ad(!col_map || col_table); if (!offsets) { @@ -385,6 +386,11 @@ static inline dtuple_t *row_build_low(ulint type, const dict_index_t *index, } #if defined UNIV_DEBUG || defined UNIV_BLOB_LIGHT_DEBUG +#ifdef HAVE_LOCK_FREE_READVIEW + ut_a(!rec_offs_any_null_extern(rec, offsets) || + trx_sys->is_registered(current_trx(), + row_get_rec_trx_id(rec, index, offsets))); +#else /* Some blob refs can be NULL during crash recovery before trx_rollback_active() has completed execution, or when a concurrently executing insert or update has committed the B-tree mini-transaction @@ -394,6 +400,7 @@ static inline dtuple_t *row_build_low(ulint type, const dict_index_t *index, insert or update statement. */ ut_a(!rec_offs_any_null_extern(rec, offsets) || trx_rw_is_active(row_get_rec_trx_id(rec, index, offsets), NULL, false)); +#endif /* HAVE_LOCK_FREE_READVIEW */ #endif /* UNIV_DEBUG || UNIV_BLOB_LIGHT_DEBUG */ if (type != ROW_COPY_POINTERS) { diff --git a/storage/innobase/row/row0sel.cc b/storage/innobase/row/row0sel.cc index ede4d37..07f3e63 100644 --- a/storage/innobase/row/row0sel.cc +++ b/storage/innobase/row/row0sel.cc @@ -751,9 +751,15 @@ static void row_sel_build_committed_vers_for_mysql( prebuilt->old_vers_heap = mem_heap_create(rec_offs_size(*offsets)); } +#ifdef HAVE_LOCK_FREE_READVIEW + row_vers_build_for_semi_consistent_read( + prebuilt->trx, rec, mtr, clust_index, offsets, offset_heap, + prebuilt->old_vers_heap, old_vers, vrow); +#else row_vers_build_for_semi_consistent_read(rec, mtr, clust_index, offsets, offset_heap, prebuilt->old_vers_heap, old_vers, vrow); +#endif /* HAVE_LOCK_FREE_READVIEW */ } /** Tests the conditions which determine when the index segment we are searching @@ -2165,6 +2171,16 @@ que_thr_t *row_sel_step(que_thr_t *thr) /*!< in: query thread */ if (node->consistent_read) { /* Assign a read view for the query */ +#ifdef HAVE_LOCK_FREE_READVIEW + trx_t *trx = thr_get_trx(thr); + trx->read_view.open(trx); + + if (trx->read_view.is_open()) { + node->read_view = &trx->read_view; + } else { + node->read_view = nullptr; + } +#else trx_assign_read_view(thr_get_trx(thr)); if (thr_get_trx(thr)->read_view != NULL) { @@ -2172,6 +2188,7 @@ que_thr_t *row_sel_step(que_thr_t *thr) /*!< in: query thread */ } else { node->read_view = NULL; } +#endif /* HAVE_LOCK_FREE_READVIEW */ } else { sym_node_t *table_node; @@ -3346,9 +3363,15 @@ dberr_t Row_sel_get_clust_rec_for_mysql::operator()( trx_get_read_view(trx))) { if (clust_rec != cached_clust_rec) { /* The following call returns 'offsets' associated with 'old_vers' */ +#ifdef HAVE_LOCK_FREE_READVIEW + err = row_sel_build_prev_vers_for_mysql( + &trx->read_view, clust_index, prebuilt, clust_rec, offsets, + offset_heap, &old_vers, vrow, mtr, lob_undo); +#else err = row_sel_build_prev_vers_for_mysql( trx->read_view, clust_index, prebuilt, clust_rec, offsets, offset_heap, &old_vers, vrow, mtr, lob_undo); +#endif /* HAVE_LOCK_FREE_READVIEW */ if (err != DB_SUCCESS) { goto err_exit; @@ -4644,7 +4667,12 @@ dberr_t row_search_mvcc(byte *buf, page_cur_mode_t mode, if (trx->mysql_n_tables_locked == 0 && !prebuilt->ins_sel_stmt && prebuilt->select_lock_type == LOCK_NONE && trx->isolation_level > TRX_ISO_READ_UNCOMMITTED && - MVCC::is_view_active(trx->read_view)) { +#ifdef HAVE_LOCK_FREE_READVIEW + trx->read_view.is_open()) +#else + MVCC::is_view_active(trx->read_view)) +#endif /* HAVE_LOCK_FREE_READVIEW */ + { /* This is a SELECT query done as a consistent read, and the read view has already been allocated: let us try a search shortcut through the hash @@ -4753,8 +4781,13 @@ dberr_t row_search_mvcc(byte *buf, page_cur_mode_t mode, ut_ad(!trx_is_started(trx) || trx->state == TRX_STATE_ACTIVE); +#ifdef HAVE_LOCK_FREE_READVIEW + ut_ad(prebuilt->sql_stat_start || prebuilt->select_lock_type != LOCK_NONE || + trx->read_view.is_open() || srv_read_only_mode); +#else ut_ad(prebuilt->sql_stat_start || prebuilt->select_lock_type != LOCK_NONE || MVCC::is_view_active(trx->read_view) || srv_read_only_mode); +#endif /* HAVE_LOCK_FREE_READVIEW */ trx_start_if_not_started(trx, false); @@ -4797,6 +4830,17 @@ dberr_t row_search_mvcc(byte *buf, page_cur_mode_t mode, if (!prebuilt->sql_stat_start) { /* No need to set an intention lock or assign a read view */ +#ifdef HAVE_LOCK_FREE_READVIEW + if (!trx->read_view.is_open() && !srv_read_only_mode && + prebuilt->select_lock_type == LOCK_NONE) { + ib::error(ER_IB_MSG_1031) << "MySQL is trying to perform a" + " consistent read but the read view is not" + " assigned!"; + trx_print(stderr, trx, 600); + fputc('\n', stderr); + ut_error; + } +#else if (!MVCC::is_view_active(trx->read_view) && !srv_read_only_mode && prebuilt->select_lock_type == LOCK_NONE) { ib::error(ER_IB_MSG_1031) << "MySQL is trying to perform a" @@ -4806,12 +4850,17 @@ dberr_t row_search_mvcc(byte *buf, page_cur_mode_t mode, fputc('\n', stderr); ut_error; } +#endif /* HAVE_LOCK_FREE_READVIEW */ } else if (prebuilt->select_lock_type == LOCK_NONE) { /* This is a consistent read */ /* Assign a read view for the query */ if (!srv_read_only_mode) { +#ifdef HAVE_LOCK_FREE_READVIEW + trx->read_view.open(trx); +#else trx_assign_read_view(trx); +#endif /* HAVE_LOCK_FREE_READVIEW */ } prebuilt->sql_stat_start = FALSE; @@ -5315,10 +5364,17 @@ rec_loop: trx_get_read_view(trx))) { rec_t *old_vers; /* The following call returns 'offsets' associated with 'old_vers' */ +#ifdef HAVE_LOCK_FREE_READVIEW + err = row_sel_build_prev_vers_for_mysql( + &trx->read_view, clust_index, prebuilt, rec, &offsets, &heap, + &old_vers, need_vrow ? &vrow : nullptr, &mtr, + prebuilt->get_lob_undo()); +#else err = row_sel_build_prev_vers_for_mysql( trx->read_view, clust_index, prebuilt, rec, &offsets, &heap, &old_vers, need_vrow ? &vrow : NULL, &mtr, prebuilt->get_lob_undo()); +#endif /* HAVE_LOCK_FREE_READVIEW */ if (err != DB_SUCCESS) { goto lock_wait_or_error; @@ -5343,8 +5399,14 @@ rec_loop: ut_ad(!index->is_clustered()); +#ifdef HAVE_LOCK_FREE_READVIEW + if (!srv_read_only_mode && + !lock_sec_rec_cons_read_sees(rec, index, &trx->read_view)) +#else if (!srv_read_only_mode && - !lock_sec_rec_cons_read_sees(rec, index, trx->read_view)) { + !lock_sec_rec_cons_read_sees(rec, index, trx->read_view)) +#endif /* HAVE_LOCK_FREE_READVIEW */ + { /* We should look at the clustered index. However, as this is a non-locking read, we can skip the clustered index lookup if diff --git a/storage/innobase/row/row0upd.cc b/storage/innobase/row/row0upd.cc index 5e950b2..ac7aa5f 100644 --- a/storage/innobase/row/row0upd.cc +++ b/storage/innobase/row/row0upd.cc @@ -3054,8 +3054,14 @@ static MY_ATTRIBUTE((warn_unused_result)) dberr_t } } +#ifdef HAVE_LOCK_FREE_READVIEW + ut_ad((row_get_rec_trx_id(rec, index, offsets) == trx->id) || + lock_trx_has_rec_x_lock(thr, index->table, btr_pcur_get_block(pcur), + page_rec_get_heap_no(rec))); +#else ut_ad(lock_trx_has_rec_x_lock(thr, index->table, btr_pcur_get_block(pcur), page_rec_get_heap_no(rec))); +#endif /* HAVE_LOCK_FREE_READVIEW */ /* NOTE: the following function calls will also commit mtr */ diff --git a/storage/innobase/row/row0vers.cc b/storage/innobase/row/row0vers.cc index 42ab246..6a4b322 100644 --- a/storage/innobase/row/row0vers.cc +++ b/storage/innobase/row/row0vers.cc @@ -228,6 +228,9 @@ looking_for_match to the given sec_rec is found among versions created by trx_id or the one version before them */ static bool row_vers_find_matching( +#ifdef HAVE_LOCK_FREE_READVIEW + trx_t *caller_trx, /*!< in/out: trx of current thread */ +#endif /* HAVE_LOCK_FREE_READVIEW */ bool looking_for_match, const dict_index_t *const clust_index, const rec_t *const clust_rec, ulint *&clust_offsets, const dict_index_t *const sec_index, const rec_t *const sec_rec, @@ -255,8 +258,13 @@ static bool row_vers_find_matching( /* The oldest visible clustered index version must not be delete-marked, because we never start a transaction by inserting a delete-marked record. */ +#ifdef HAVE_LOCK_FREE_READVIEW + ut_ad(prev_version != nullptr || rec_get_deleted_flag(version, comp) == 0 || + !trx_sys->is_registered(caller_trx, trx_id)); +#else ut_ad(prev_version || !rec_get_deleted_flag(version, comp) || !trx_rw_is_active(trx_id, NULL, false)); +#endif /* HAVE_LOCK_FREE_READVIEW */ /* Free version and clust_offsets. */ mem_heap_free(old_heap); @@ -289,6 +297,9 @@ static bool row_vers_find_matching( trx_is_active() while holding lock_sys->mutex. */ UNIV_INLINE trx_t *row_vers_impl_x_locked_low( +#ifdef HAVE_LOCK_FREE_READVIEW + trx_t *caller_trx, /*!< in/out: trx of current thread */ +#endif /* HAVE_LOCK_FREE_READVIEW */ const rec_t *const clust_rec, /*!< in: clustered index record */ const dict_index_t *const clust_index, /*!< in: the clustered index */ const rec_t *const sec_rec, /*!< in: secondary index record */ @@ -496,6 +507,26 @@ trx_t *row_vers_impl_x_locked_low( trx_id = row_get_rec_trx_id(clust_rec, clust_index, clust_offsets); corrupt = FALSE; +#ifdef HAVE_LOCK_FREE_READVIEW + trx_t *trx; + + if (trx_id == caller_trx->id) { + trx = caller_trx; + trx->reference(); + } else { + trx = trx_sys->find(caller_trx, trx_id); + if (trx == nullptr) { + /* The transaction that modified or inserted clust_rec is no + longer active, or it is corrupt: no implicit lock on rec */ + if (corrupt != 0u) { + lock_report_trx_id_insanity(trx_id, clust_rec, clust_index, + clust_offsets, trx_sys->get_max_trx_id()); + } + mem_heap_free(heap); + return 0; + } + } +#else trx_t *trx = trx_rw_is_active(trx_id, &corrupt, true); if (trx == 0) { @@ -508,6 +539,7 @@ trx_t *row_vers_impl_x_locked_low( mem_heap_free(heap); return 0; } +#endif /* HAVE_LOCK_FREE_READVIEW */ comp = page_rec_is_comp(sec_rec); ut_ad(sec_index->table == clust_index->table); @@ -516,10 +548,17 @@ trx_t *row_vers_impl_x_locked_low( bool looking_for_match = rec_get_deleted_flag(sec_rec, comp); +#ifdef HAVE_LOCK_FREE_READVIEW + if (!row_vers_find_matching(caller_trx, looking_for_match, clust_index, + clust_rec, clust_offsets, sec_index, sec_rec, + sec_offsets, comp, trx_id, mtr, heap)) { + trx->release_reference(); +#else if (!row_vers_find_matching(looking_for_match, clust_index, clust_rec, clust_offsets, sec_index, sec_rec, sec_offsets, comp, trx_id, mtr, heap)) { trx_release_reference(trx); +#endif /* HAVE_LOCK_FREE_READVIEW */ trx = 0; } @@ -536,6 +575,9 @@ trx_t *row_vers_impl_x_locked_low( negatives. The caller must confirm all positive results by calling trx_is_active() while holding lock_sys->mutex. */ trx_t *row_vers_impl_x_locked( +#ifdef HAVE_LOCK_FREE_READVIEW + trx_t *caller_trx, /*!< in/out: trx of current thread */ +#endif /* HAVE_LOCK_FREE_READVIEW */ const rec_t *rec, /*!< in: record in a secondary index */ const dict_index_t *index, /*!< in: the secondary index */ const ulint *offsets) /*!< in: rec_get_offsets(rec, index) */ @@ -575,10 +617,17 @@ trx_t *row_vers_impl_x_locked( trx = 0; } else { +#ifdef HAVE_LOCK_FREE_READVIEW + trx = row_vers_impl_x_locked_low(caller_trx, clust_rec, clust_index, rec, + index, offsets, &mtr); + + ut_ad(trx == nullptr || trx->is_referenced()); +#else trx = row_vers_impl_x_locked_low(clust_rec, clust_index, rec, index, offsets, &mtr); ut_ad(trx == 0 || trx_is_referenced(trx)); +#endif /* HAVE_LOCK_FREE_READVIEW */ } mtr_commit(&mtr); @@ -1357,6 +1406,9 @@ dberr_t row_vers_build_for_consistent_read( /** Constructs the last committed version of a clustered index record, which should be seen by a semi-consistent read. */ void row_vers_build_for_semi_consistent_read( +#ifdef HAVE_LOCK_FREE_READVIEW + trx_t *caller_trx, /*!< in/out: trx of current thread */ +#endif /* HAVE_LOCK_FREE_READVIEW */ const rec_t *rec, /*!< in: record in a clustered index; the caller must have a latch on the page; this latch locks the top of the stack of versions @@ -1393,7 +1445,9 @@ void row_vers_build_for_semi_consistent_read( ut_ad(!vrow || !(*vrow)); for (;;) { +#ifndef HAVE_LOCK_FREE_READVIEW const trx_t *version_trx; +#endif /* HAVE_LOCK_FREE_READVIEW */ mem_heap_t *heap2; rec_t *prev_version; trx_id_t version_trx_id; @@ -1403,6 +1457,9 @@ void row_vers_build_for_semi_consistent_read( rec_trx_id = version_trx_id; } +#ifdef HAVE_LOCK_FREE_READVIEW + if (!trx_sys->is_registered(caller_trx, version_trx_id)) +#else trx_sys_mutex_enter(); version_trx = trx_get_rw_trx_by_id(version_trx_id); /* Because version_trx is a read-write transaction, @@ -1415,7 +1472,9 @@ void row_vers_build_for_semi_consistent_read( } trx_sys_mutex_exit(); - if (!version_trx) { + if (!version_trx) +#endif /* HAVE_LOCK_FREE_READVIEW */ + { committed_version_trx: /* We found a version that belongs to a committed transaction: return it. */ diff --git a/storage/innobase/srv/srv0srv.cc b/storage/innobase/srv/srv0srv.cc index 9592955..3ee3a68 100644 --- a/storage/innobase/srv/srv0srv.cc +++ b/storage/innobase/srv/srv0srv.cc @@ -1421,9 +1421,14 @@ ibool srv_printf_innodb_monitor( ULINTPF " queries inside InnoDB, " ULINTPF " queries in queue\n", srv_conc_get_active_threads(), srv_conc_get_waiting_threads()); +#ifdef HAVE_LOCK_FREE_READVIEW + fprintf(file, ULINTPF " read views open inside InnoDB\n", + trx_sys->view_count()); +#else /* This is a dirty read, without holding trx_sys->mutex. */ fprintf(file, ULINTPF " read views open inside InnoDB\n", trx_sys->mvcc->size()); +#endif /* HAVE_LOCK_FREE_READVIEW */ n_reserved = fil_space_get_n_reserved_extents(0); if (n_reserved > 0) { @@ -1636,6 +1641,10 @@ void srv_export_innodb_status(void) { export_vars.innodb_undo_tablespaces_implicit = FSP_IMPLICIT_UNDO_TABLESPACES; +#ifdef HAVE_LOCK_FREE_READVIEW + export_vars.innodb_have_lock_free_readview = TRUE; +#endif + undo::spaces->s_lock(); export_vars.innodb_undo_tablespaces_total = undo::spaces->size(); @@ -1660,15 +1669,23 @@ void srv_export_innodb_status(void) { transaction number. We are allowed to purge transactions with number below the low limit. */ ReadView oldest_view; +#ifdef HAVE_LOCK_FREE_READVIEW + trx_sys->clone_oldest_view(&oldest_view); +#else trx_sys->mvcc->clone_oldest_view(&oldest_view); +#endif /* HAVE_LOCK_FREE_READVIEW */ trx_id_t low_limit_no = oldest_view.view_low_limit_no(); rw_lock_s_unlock(&purge_sys->latch); +#ifdef HAVE_LOCK_FREE_READVIEW + trx_id_t max_trx_no = trx_sys->rw_max_trx_no; +#else mutex_enter(&trx_sys->mutex); /* Maximum transaction number added to history list for purge. */ trx_id_t max_trx_no = trx_sys->rw_max_trx_no; mutex_exit(&trx_sys->mutex); +#endif /* HAVE_LOCK_FREE_READVIEW */ if (done_trx_no == 0 || max_trx_no < done_trx_no) { export_vars.innodb_purge_trx_id_age = 0; diff --git a/storage/innobase/srv/srv0start.cc b/storage/innobase/srv/srv0start.cc index 9ed698d..c62f27f 100644 --- a/storage/innobase/srv/srv0start.cc +++ b/storage/innobase/srv/srv0start.cc @@ -1345,10 +1345,17 @@ dberr_t srv_undo_tablespaces_upgrade() { rsegs and undo tablespaces they are in from being deleted. These transactions must be either committed or rolled back by the mysql server.*/ +#ifdef HAVE_LOCK_FREE_READVIEW + if (trx_sys->found_prepared_trx()) { + ib::warn(ER_IB_MSG_1094); + return (DB_SUCCESS); + } +#else if (trx_sys->n_prepared_trx > 0) { ib::warn(ER_IB_MSG_1094); return (DB_SUCCESS); } +#endif /* HAVE_LOCK_FREE_READVIEW */ ib::info(ER_IB_MSG_1095, trx_sys_undo_spaces->size(), ulong{FSP_IMPLICIT_UNDO_TABLESPACES}); @@ -2880,9 +2887,15 @@ void srv_dict_recover_on_restart() { that the data dictionary tables will be free of any locks. The data dictionary latch should guarantee that there is at most one data dictionary transaction active at a time. */ +#ifdef HAVE_LOCK_FREE_READVIEW + if (srv_force_recovery < SRV_FORCE_NO_TRX_UNDO) { + trx_rollback_recovered(false); + } +#else if (srv_force_recovery < SRV_FORCE_NO_TRX_UNDO && trx_sys_need_rollback()) { trx_rollback_or_clean_recovered(FALSE); } +#endif /* HAVE_LOCK_FREE_READVIEW */ /* Do after all DD transactions recovery, to get consistent metadata */ apply_dynamic_metadata(); @@ -2963,6 +2976,16 @@ void srv_start_threads(bool bootstrap) { return; } +#ifdef HAVE_LOCK_FREE_READVIEW + if (!bootstrap && srv_force_recovery < SRV_FORCE_NO_TRX_UNDO) { + /* Rollback all recovered transactions that are + not in committed nor in XA PREPARE state. */ + srv_threads.m_trx_recovery_rollback = os_thread_create( + trx_recovery_rollback_thread_key, trx_recovery_rollback_thread); + + srv_threads.m_trx_recovery_rollback.start(); + } +#else if (!bootstrap && srv_force_recovery < SRV_FORCE_NO_TRX_UNDO && trx_sys_need_rollback()) { /* Rollback all recovered transactions that are @@ -2972,6 +2995,7 @@ void srv_start_threads(bool bootstrap) { srv_threads.m_trx_recovery_rollback.start(); } +#endif /* HAVE_LOCK_FREE_READVIEW */ /* Create the master thread which does purge and other utility operations */ @@ -3096,7 +3120,11 @@ void srv_pre_dd_shutdown() { prepared transactions and we don't want to lose them. */ for (uint32_t count = 0;; ++count) { +#ifdef HAVE_LOCK_FREE_READVIEW + const ulint total_trx = trx_sys->any_active_transactions(); +#else const ulint total_trx = trx_sys_any_active_transactions(); +#endif if (total_trx == 0) { break; @@ -3187,7 +3215,11 @@ void srv_pre_dd_shutdown() { srv_is_being_shutdown = true; +#ifdef HAVE_LOCK_FREE_READVIEW + ut_a(trx_sys->any_active_transactions() == 0); +#else ut_a(trx_sys_any_active_transactions() == 0); +#endif DBUG_EXECUTE_IF("wait_for_threads_in_pre_dd_shutdown", srv_shutdown_background_threads();); @@ -3468,7 +3500,11 @@ void srv_shutdown() { ut_a(!srv_is_being_started); ut_a(srv_is_being_shutdown); +#ifdef HAVE_LOCK_FREE_READVIEW + ut_a(trx_sys->any_active_transactions() == 0); +#else ut_a(trx_sys_any_active_transactions() == 0); +#endif /* Ensure threads below have been stopped. */ const auto threads_stopped_before_shutdown = { diff --git a/storage/innobase/sync/sync0debug.cc b/storage/innobase/sync/sync0debug.cc index 0267a9a..675d3ed 100644 --- a/storage/innobase/sync/sync0debug.cc +++ b/storage/innobase/sync/sync0debug.cc @@ -444,6 +444,9 @@ LatchDebug::LatchDebug() { LEVEL_MAP_INSERT(SYNC_REC_LOCK); LEVEL_MAP_INSERT(SYNC_THREADS); LEVEL_MAP_INSERT(SYNC_TRX); +#ifdef HAVE_LOCK_FREE_READVIEW + LEVEL_MAP_INSERT(SYNC_RW_TRX_HASH_ELEMENT); +#endif /* HAVE_LOCK_FREE_READVIEW */ LEVEL_MAP_INSERT(SYNC_TRX_SYS); LEVEL_MAP_INSERT(SYNC_LOCK_SYS); LEVEL_MAP_INSERT(SYNC_LOCK_WAIT_SYS); @@ -481,6 +484,9 @@ LatchDebug::LatchDebug() { LEVEL_MAP_INSERT(SYNC_TABLE); LEVEL_MAP_INSERT(SYNC_STATS_AUTO_RECALC); LEVEL_MAP_INSERT(SYNC_DICT_AUTOINC_MUTEX); +#ifdef HAVE_SCALABLE_LOCK_MGR + LEVEL_MAP_INSERT(SYNC_DICT_TABLE_LOCKS_MUTEX); +#endif /* HAVE_SCALABLE_LOCK_MGR */ LEVEL_MAP_INSERT(SYNC_DICT); LEVEL_MAP_INSERT(SYNC_PARSER); LEVEL_MAP_INSERT(SYNC_FTS_CACHE); @@ -705,6 +711,9 @@ Latches *LatchDebug::check_order(const latch_t *latch, case SYNC_THREADS: case SYNC_LOCK_SYS: case SYNC_LOCK_WAIT_SYS: +#ifdef HAVE_LOCK_FREE_READVIEW + case SYNC_RW_TRX_HASH_ELEMENT: +#endif /* HAVE_LOCK_FREE_READVIEW */ case SYNC_TRX_SYS: case SYNC_IBUF_BITMAP_MUTEX: case SYNC_TEMP_SPACE_RSEG: @@ -1490,6 +1499,11 @@ static void sync_latch_meta_init() UNIV_NOTHROW { LATCH_ADD_MUTEX(TEST_MUTEX, SYNC_NO_ORDER_CHECK, PFS_NOT_INSTRUMENTED); +#ifdef HAVE_LOCK_FREE_READVIEW + LATCH_ADD_MUTEX(RW_TRX_HASH_ELEMENT, SYNC_RW_TRX_HASH_ELEMENT, + rw_trx_hash_element_mutex_key); +#endif /* HAVE_LOCK_FREE_READVIEW */ + latch_id_t id = LATCH_ID_NONE; /* The array should be ordered on latch ID.We need to diff --git a/storage/innobase/sync/sync0sync.cc b/storage/innobase/sync/sync0sync.cc index 4330303..86bd999 100644 --- a/storage/innobase/sync/sync0sync.cc +++ b/storage/innobase/sync/sync0sync.cc @@ -125,6 +125,9 @@ mysql_pfs_key_t trx_pool_manager_mutex_key; mysql_pfs_key_t temp_pool_manager_mutex_key; mysql_pfs_key_t lock_mutex_key; mysql_pfs_key_t lock_wait_mutex_key; +#ifdef HAVE_LOCK_FREE_READVIEW +mysql_pfs_key_t rw_trx_hash_element_mutex_key; +#endif /* HAVE_LOCK_FREE_READVIEW */ mysql_pfs_key_t trx_sys_mutex_key; mysql_pfs_key_t srv_sys_mutex_key; mysql_pfs_key_t srv_threads_mutex_key; diff --git a/storage/innobase/trx/trx0i_s.cc b/storage/innobase/trx/trx0i_s.cc index e0dbe8e..c17fb67 100644 --- a/storage/innobase/trx/trx0i_s.cc +++ b/storage/innobase/trx/trx0i_s.cc @@ -860,6 +860,29 @@ static void trx_i_s_cache_clear( /** Fetches the data needed to fill the 3 INFORMATION SCHEMA tables into the table cache buffer. Cache must be locked for write. */ +#ifdef HAVE_LOCK_FREE_READVIEW +static void fetch_data_into_cache_low( + trx_i_s_cache_t *cache, /*!< in/out: cache */ + const trx_t *trx) /*!< in/out: transaction */ +{ + i_s_locks_row_t *requested_lock_row; + + assert_trx_nonlocking_or_in_list(trx); + if (add_trx_relevant_locks_to_cache(cache, trx, &requested_lock_row) != 0u) { + auto trx_row = static_cast( + table_cache_create_empty_row(&cache->innodb_trx, cache)); + if (trx_row != nullptr) { + if (fill_trx_row(trx_row, trx, requested_lock_row, cache) == TRUE) { + return; + } + --cache->innodb_trx.rows_used; + } + } + + /* memory could not be allocated */ + cache->is_truncated = TRUE; +} +#else static void fetch_data_into_cache_low( trx_i_s_cache_t *cache, /*!< in/out: cache */ bool read_write, /*!< in: only read-write @@ -923,22 +946,39 @@ static void fetch_data_into_cache_low( trx_mutex_exit(trx); } } +#endif /* HAVE_LOCK_FREE_READVIEW */ /** Fetches the data needed to fill the 3 INFORMATION SCHEMA tables into the table cache buffer. Cache must be locked for write. */ static void fetch_data_into_cache(trx_i_s_cache_t *cache) /*!< in/out: cache */ { ut_ad(lock_mutex_own()); +#ifndef HAVE_LOCK_FREE_READVIEW ut_ad(trx_sys_mutex_own()); +#endif /* HAVE_LOCK_FREE_READVIEW */ trx_i_s_cache_clear(cache); +#ifdef HAVE_LOCK_FREE_READVIEW + mutex_enter(&trx_sys->mutex); + for (const trx_t *trx = UT_LIST_GET_FIRST(trx_sys->trx_list); trx != nullptr; + trx = UT_LIST_GET_NEXT(trx_list, trx)) { + if (trx_is_started(trx) && trx != purge_sys->query->trx) { + fetch_data_into_cache_low(cache, trx); + if (cache->is_truncated == TRUE) { + break; + } + } + } + mutex_exit(&trx_sys->mutex); +#else /* Capture the state of the read-write transactions. This includes internal transactions too. They are not on mysql_trx_list */ fetch_data_into_cache_low(cache, true, &trx_sys->rw_trx_list); /* Capture the state of the read-only active transactions */ fetch_data_into_cache_low(cache, false, &trx_sys->mysql_trx_list); +#endif /* HAVE_LOCK_FREE_READVIEW */ cache->is_truncated = FALSE; } @@ -957,11 +997,15 @@ int trx_i_s_possibly_fetch_data_into_cache( lock_mutex_enter(); +#ifndef HAVE_LOCK_FREE_READVIEW trx_sys_mutex_enter(); +#endif /* HAVE_LOCK_FREE_READVIEW */ fetch_data_into_cache(cache); +#ifndef HAVE_LOCK_FREE_READVIEW trx_sys_mutex_exit(); +#endif /* HAVE_LOCK_FREE_READVIEW */ lock_mutex_exit(); diff --git a/storage/innobase/trx/trx0purge.cc b/storage/innobase/trx/trx0purge.cc index f6d2193..583ccb0 100644 --- a/storage/innobase/trx/trx0purge.cc +++ b/storage/innobase/trx/trx0purge.cc @@ -240,7 +240,11 @@ void trx_purge_sys_create(ulint n_purge_threads, purge_pq_t *purge_queue) { new (&purge_sys->view) ReadView(); +#ifdef HAVE_LOCK_FREE_READVIEW + trx_sys->clone_oldest_view(); +#else trx_sys->mvcc->clone_oldest_view(&purge_sys->view); +#endif /* HAVE_LOCK_FREE_READVIEW */ purge_sys->view_active = true; @@ -348,7 +352,11 @@ void trx_purge_add_update_undo_to_history( undo_header + TRX_UNDO_HISTORY_NODE, mtr); if (update_rseg_history_len) { +#ifdef HAVE_LOCK_FREE_READVIEW + trx_sys->rseg_history_len.fetch_add(n_added_logs); +#else os_atomic_increment_ulint(&trx_sys->rseg_history_len, n_added_logs); +#endif /* HAVE_LOCK_FREE_READVIEW */ srv_wake_purge_thread_if_not_active(); } @@ -384,7 +392,11 @@ static void trx_purge_remove_log_hdr(trx_rsegf_t *rseg_hdr, flst_remove(rseg_hdr + TRX_RSEG_HISTORY, log_hdr + TRX_UNDO_HISTORY_NODE, mtr); +#ifdef HAVE_LOCK_FREE_READVIEW + trx_sys->rseg_history_len.fetch_sub(1); +#else os_atomic_decrement_ulint(&trx_sys->rseg_history_len, 1); +#endif /* HAVE_LOCK_FREE_READVIEW */ } /** Frees a rollback segment which is in the history list. @@ -1655,7 +1667,9 @@ static void trx_purge_rseg_get_next_history_log( mtr_commit(&mtr); #ifdef UNIV_DEBUG +#ifndef HAVE_LOCK_FREE_READVIEW trx_sys_mutex_enter(); +#endif /* HAVE_LOCK_FREE_READVIEW */ /* Add debug code to track history list corruption reported on the MySQL mailing list on Nov 9, 2004. The fut0lst.cc @@ -1680,7 +1694,9 @@ static void trx_purge_rseg_get_next_history_log( " threads to expedite purging of undo logs."; } +#ifndef HAVE_LOCK_FREE_READVIEW trx_sys_mutex_exit(); +#endif /* HAVE_LOCK_FREE_READVIEW */ #endif return; } @@ -2214,7 +2230,11 @@ ulint trx_purge(ulint n_purge_threads, /*!< in: number of purge tasks purge_sys->view_active = false; +#ifdef HAVE_LOCK_FREE_READVIEW + trx_sys->clone_oldest_view(); +#else trx_sys->mvcc->clone_oldest_view(&purge_sys->view); +#endif /* HAVE_LOCK_FREE_READVIEW */ purge_sys->view_active = true; diff --git a/storage/innobase/trx/trx0roll.cc b/storage/innobase/trx/trx0roll.cc index 861d3f3..fe805b2 100644 --- a/storage/innobase/trx/trx0roll.cc +++ b/storage/innobase/trx/trx0roll.cc @@ -186,11 +186,19 @@ static dberr_t trx_rollback_low(trx_t *trx) { case TRX_STATE_FORCED_ROLLBACK: case TRX_STATE_NOT_STARTED: trx->will_lock = 0; +#ifdef HAVE_LOCK_FREE_READVIEW + ut_ad(trx->mysql_thd != nullptr); +#else ut_ad(trx->in_mysql_trx_list); +#endif /* HAVE_LOCK_FREE_READVIEW */ return (DB_SUCCESS); case TRX_STATE_ACTIVE: +#ifdef HAVE_LOCK_FREE_READVIEW + ut_ad(trx->mysql_thd != nullptr); +#else ut_ad(trx->in_mysql_trx_list); +#endif /* HAVE_LOCK_FREE_READVIEW */ assert_trx_nonlocking_or_in_list(trx); /* Check an validate that undo is available for GTID. */ trx_undo_gtid_add_update_undo(trx, false, true); @@ -285,7 +293,11 @@ dberr_t trx_rollback_last_sql_stat_for_mysql( here, because the statement rollback should be invoked for a running active MySQL transaction that is associated with the current thread. */ +#ifdef HAVE_LOCK_FREE_READVIEW + ut_ad(trx->mysql_thd != nullptr); +#else ut_ad(trx->in_mysql_trx_list); +#endif /* HAVE_LOCK_FREE_READVIEW */ switch (trx->state) { case TRX_STATE_FORCED_ROLLBACK: @@ -390,7 +402,11 @@ executed after the savepoint */ dberr_t err; ut_ad(trx_state_eq(trx, TRX_STATE_ACTIVE)); +#ifdef HAVE_LOCK_FREE_READVIEW + ut_ad(trx->mysql_thd != nullptr); +#else ut_ad(trx->in_mysql_trx_list); +#endif /* HAVE_LOCK_FREE_READVIEW */ /* Free all savepoints strictly later than savep. */ @@ -437,7 +453,11 @@ dberr_t trx_rollback_to_savepoint_for_mysql( here, because the savepoint rollback should be invoked for a running active MySQL transaction that is associated with the current thread. */ +#ifdef HAVE_LOCK_FREE_READVIEW + ut_ad(trx->mysql_thd != nullptr); +#else ut_ad(trx->in_mysql_trx_list); +#endif /* HAVE_LOCK_FREE_READVIEW */ savep = trx_savepoint_find(trx, savepoint_name); @@ -524,7 +544,11 @@ dberr_t trx_release_savepoint_for_mysql( trx_named_savept_t *savep; ut_ad(trx_state_eq(trx, TRX_STATE_ACTIVE)); +#ifdef HAVE_LOCK_FREE_READVIEW + ut_ad(trx->mysql_thd != nullptr); +#else ut_ad(trx->in_mysql_trx_list); +#endif /* HAVE_LOCK_FREE_READVIEW */ savep = trx_savepoint_find(trx, savepoint_name); @@ -581,7 +605,9 @@ static void trx_rollback_active(trx_t *trx) /*!< in/out: transaction */ ut_a(thr == que_fork_start_command(fork)); +#ifndef HAVE_LOCK_FREE_READVIEW trx_sys_mutex_enter(); +#endif /* HAVE_LOCK_FREE_READVIEW */ trx_roll_crash_recv_trx = trx; @@ -591,7 +617,9 @@ static void trx_rollback_active(trx_t *trx) /*!< in/out: transaction */ rows_to_undo = trx_roll_max_undo_no; +#ifndef HAVE_LOCK_FREE_READVIEW trx_sys_mutex_exit(); +#endif /* HAVE_LOCK_FREE_READVIEW */ if (rows_to_undo > 1000000000) { rows_to_undo = rows_to_undo / 1000000; @@ -623,6 +651,71 @@ static void trx_rollback_active(trx_t *trx) /*!< in/out: transaction */ trx_roll_crash_recv_trx = NULL; } +#ifdef HAVE_LOCK_FREE_READVIEW +static bool trx_rollback_recovered_callback(rw_trx_hash_element_t *element, + std::vector *trx_list) { + mutex_enter(&element->mutex); + trx_t *trx = element->trx; + if (trx != nullptr) { + mutex_enter(&trx->mutex); + if (trx->is_recovered && trx_state_eq(trx, TRX_STATE_ACTIVE)) { + ut_ad(trx != trx_dummy_sess->trx); + trx_list->push_back(trx); + } + mutex_exit(&trx->mutex); + } + mutex_exit(&element->mutex); + return false; +} + +/** Rollback any incomplete transactions which were encountered in crash + recovery. + + If the transaction already was committed, then we cleanup a possible insert + undo log. If the transaction was not yet committed, then we roll it back. + + Note: For XA recovered transactions, we rely on MySQL to do rollback. They + will be in TRX_STATE_PREPARED state. If the server is shutdown and they are + still lingering in trx_sys_t::trx_list then the shutdown will hang. + + @param [in] all true=roll back all recovered active transactions; + fasle=roll back any incomplete dictionary transaction */ +void trx_rollback_recovered(bool all) { + std::vector trx_list; + + ut_a(srv_force_recovery < SRV_FORCE_NO_TRX_UNDO); + + /* Collect list of recovered ACTIVE transaction ids first. Once collected, no + other thread is allowed to modify or remove these transactions from + rw_trx_hash.*/ + trx_sys->rw_trx_hash.iterate_no_dups( + reinterpret_cast(trx_rollback_recovered_callback), + &trx_list); + + while (!trx_list.empty()) { + trx_t *trx = trx_list.back(); + trx_list.pop_back(); + +#ifdef UNIV_DEBUG + ut_ad(trx != nullptr); + trx_mutex_enter(trx); + ut_ad(trx->is_recovered && trx_state_eq(trx, TRX_STATE_ACTIVE)); + trx_mutex_exit(trx); +#endif /* UNIV_DEBUG */ + + if (all || trx_get_dict_operation(trx) != TRX_DICT_OP_NONE) { + trx_rollback_active(trx); + if (trx->error_state != DB_SUCCESS) { + trx->error_state = DB_SUCCESS; + trx_sys->deregister_rw(trx); + trx_free_at_shutdown(trx); + } else { + trx_free(trx); + } + } + } +} +#else /** Rollback or clean up any resurrected incomplete transactions. It assumes that the caller holds the trx_sys_t::mutex and it will release the lock if it does a clean up or rollback. @@ -730,6 +823,7 @@ void trx_rollback_or_clean_recovered( " completed"; } } +#endif /* HAVE_LOCK_FREE_READVIEW */ /** Rollback or clean up any incomplete transactions which were encountered in crash recovery. If the transaction already was @@ -746,7 +840,17 @@ void trx_recovery_rollback_thread() { ut_ad(!srv_read_only_mode); +#ifdef HAVE_LOCK_FREE_READVIEW + if (trx_sys->rw_trx_hash.size() > 0) { + ib::info(ER_IB_MSG_1189) << "Starting in background the rollback" + " of uncommitted transactions"; + trx_rollback_recovered(true); + ib::info(ER_IB_MSG_1190) << "Rollback of non-prepared transactions" + " completed"; + } +#else trx_rollback_or_clean_recovered(TRUE); +#endif /* HAVE_LOCK_FREE_READVIEW */ destroy_thd(thd); } diff --git a/storage/innobase/trx/trx0sys.cc b/storage/innobase/trx/trx0sys.cc index f806dbd..8c5584f 100644 --- a/storage/innobase/trx/trx0sys.cc +++ b/storage/innobase/trx/trx0sys.cc @@ -49,6 +49,10 @@ this program; if not, write to the Free Software Foundation, Inc., #include "srv0srv.h" #include "srv0start.h" #include "trx0purge.h" +#ifdef HAVE_LOCK_FREE_READVIEW +#include "trx0roll.h" +#include "clone0clone.h" +#endif /* HAVE_LOCK_FREE_READVIEW */ #include "trx0rseg.h" #include "trx0trx.h" #include "trx0undo.h" @@ -67,7 +71,12 @@ void ReadView::check_trx_id_sanity(trx_id_t id, const table_name_t &name) { return; } - if (id >= trx_sys->max_trx_id) { +#ifdef HAVE_LOCK_FREE_READVIEW + if (id >= trx_sys->get_max_trx_id()) +#else + if (id >= trx_sys->max_trx_id) +#endif /* HAVE_LOCK_FREE_READVIEW */ + { ib::warn(ER_IB_MSG_1196) << "A transaction id" << " in a record of table " << name << " is newer than the" @@ -99,15 +108,22 @@ void trx_sys_flush_max_trx_id(void) { mtr_t mtr; trx_sysf_t *sys_header; +#ifndef HAVE_LOCK_FREE_READVIEW ut_ad(trx_sys_mutex_own()); +#endif /* HAVE_LOCK_FREE_READVIEW */ if (!srv_read_only_mode) { mtr_start(&mtr); sys_header = trx_sysf_get(&mtr); +#ifdef HAVE_LOCK_FREE_READVIEW + mlog_write_ull(sys_header + TRX_SYS_TRX_ID_STORE, trx_sys->get_max_trx_id(), + &mtr); +#else mlog_write_ull(sys_header + TRX_SYS_TRX_ID_STORE, trx_sys->max_trx_id, &mtr); +#endif /* HAVE_LOCK_FREE_READVIEW */ mtr_commit(&mtr); } @@ -124,6 +140,42 @@ void trx_sys_persist_gtid_num(trx_id_t gtid_trx_no) { mtr.commit(); } +#ifdef HAVE_LOCK_FREE_READVIEW +trx_id_t trx_sys_oldest_trx_no() { + /* Get the oldest transaction from serialisation list. */ + trx_id_t max_trx_id = 0; + for (auto trx = UT_LIST_GET_FIRST(trx_sys->trx_list); trx != NULL; + trx = UT_LIST_GET_NEXT(trx_list, trx)) { + if (trx->id == 0 || trx->state != TRX_STATE_ACTIVE) { + continue; + } + if (trx->no != TRX_ID_MAX && trx->no > max_trx_id) { + max_trx_id = trx->no; + } + } + return max_trx_id > 0 ? max_trx_id : trx_sys->get_max_trx_id(); +} + +void trx_sys_get_binlog_prepared(std::vector &trx_ids) { + mutex_enter(&trx_sys->mutex); + /* Exit fast if no prepared transaction. */ + if (!trx_sys->found_prepared_trx()) { + mutex_exit(&trx_sys->mutex); + return; + } + /* Check and find binary log prepared transaction. */ + for (auto trx = UT_LIST_GET_FIRST(trx_sys->trx_list); trx != NULL; + trx = UT_LIST_GET_NEXT(trx_list, trx)) { + if (trx->state != TRX_STATE_ACTIVE && trx->state != TRX_STATE_PREPARED) { + continue; + } + if (trx_state_eq(trx, TRX_STATE_PREPARED) && trx_is_mysql_xa(trx)) { + trx_ids.push_back(trx->id); + } + } + mutex_exit(&trx_sys->mutex); +} +#else trx_id_t trx_sys_oldest_trx_no() { ut_ad(trx_sys_mutex_own()); /* Get the oldest transaction from serialisation list. */ @@ -151,6 +203,7 @@ void trx_sys_get_binlog_prepared(std::vector &trx_ids) { } trx_sys_mutex_exit(); } +#endif /* HAVE_LOCK_FREE_READVIEW */ /** Read binary log positions from buffer passed. @param[in] binlog_buf binary log buffer from trx sys page @@ -440,19 +493,36 @@ purge_pq_t *trx_sys_init_at_db_start(void) { sys_header = trx_sysf_get(&mtr); +#ifdef HAVE_LOCK_FREE_READVIEW + trx_id_t max_trx_id = + 2 * TRX_SYS_TRX_ID_WRITE_MARGIN + + ut_uint64_align_up( + mach_read_from_8(sys_header + TRX_SYS_TRX_ID_STORE), // NOLINT + TRX_SYS_TRX_ID_WRITE_MARGIN); + trx_sys->init_max_trx_id(max_trx_id); +#else trx_sys->max_trx_id = 2 * TRX_SYS_TRX_ID_WRITE_MARGIN + ut_uint64_align_up(mach_read_from_8(sys_header + TRX_SYS_TRX_ID_STORE), TRX_SYS_TRX_ID_WRITE_MARGIN); +#endif /* HAVE_LOCK_FREE_READVIEW */ mtr.commit(); #ifdef UNIV_DEBUG +#ifdef HAVE_LOCK_FREE_READVIEW + /* max_trx_id is the next transaction ID to assign. Initialize maximum + transaction number to one less if all transactions are already purged. */ + if (trx_sys->rw_max_trx_no == 0) { + trx_sys->rw_max_trx_no = trx_sys->get_max_trx_id() - 1; + } +#else /* max_trx_id is the next transaction ID to assign. Initialize maximum transaction number to one less if all transactions are already purged. */ if (trx_sys->rw_max_trx_no == 0) { trx_sys->rw_max_trx_no = trx_sys->max_trx_id - 1; } +#endif #endif /* UNIV_DEBUG */ trx_dummy_sess = sess_open(); @@ -463,6 +533,44 @@ purge_pq_t *trx_sys_init_at_db_start(void) { the debug code (assertions). We are still running in single threaded bootstrap mode. */ +#ifdef HAVE_LOCK_FREE_READVIEW + mutex_enter(&trx_sys->mutex); + + if (UT_LIST_GET_LEN(trx_sys->trx_list) > 0) { + const trx_t *trx; + + for (trx = UT_LIST_GET_FIRST(trx_sys->trx_list); trx != nullptr; + trx = UT_LIST_GET_NEXT(trx_list, trx)) { + if (trx->state != TRX_STATE_ACTIVE && trx->state != TRX_STATE_PREPARED) { + /* trx_dummy_sess is a transaction in TRX_STATE_NOT_STARTED state. */ + continue; + } + ut_ad(trx->is_recovered); + ut_ad(trx->rsegs.m_redo.rseg->trx_ref_count > 0); + ut_ad(trx_sys->find(nullptr, trx->id, false) != nullptr); + + if (trx_state_eq(trx, TRX_STATE_ACTIVE)) { + rows_to_undo += trx->undo_no; + } + } + + if (rows_to_undo > 1000000000) { + unit = "M"; + rows_to_undo = rows_to_undo / 1000000; + } + + ib::info(ER_IB_MSG_1198) + << UT_LIST_GET_LEN(trx_sys->trx_list) + << " transaction(s) which must be rolled back or" + " cleaned up in total " + << rows_to_undo << unit << " row operations to undo"; + + ib::info(ER_IB_MSG_1199) + << "Trx id counter is " << trx_sys->get_max_trx_id(); + } + + mutex_exit(&trx_sys->mutex); +#else trx_sys_mutex_enter(); if (UT_LIST_GET_LEN(trx_sys->rw_trx_list) > 0) { @@ -495,10 +603,334 @@ purge_pq_t *trx_sys_init_at_db_start(void) { trx_sys->found_prepared_trx = trx_sys->n_prepared_trx > 0; trx_sys_mutex_exit(); +#endif /* HAVE_LOCK_FREE_READVIEW */ return (purge_queue); } +#ifdef HAVE_LOCK_FREE_READVIEW +/** Constructor callback for lock-free allocator. + +Object is just allocated and is not yet accessible via rw_trx_hash by +concurrent threads. Object can be reused multiple times before it is freed. +Every time object is being reused initialize() callback is called. */ +void rw_trx_hash_t::rw_trx_hash_constructor(uchar *arg) { + new (arg + LF_HASH_OVERHEAD) rw_trx_hash_element_t(); +} + +/** Destructor callback for lock-free allocator. + +Object is about to be freed and is not accessible via rw_trx_hash by +concurrent threads. */ +void rw_trx_hash_t::rw_trx_hash_destructor(uchar *arg) { + reinterpret_cast(arg + LF_HASH_OVERHEAD) + ->~rw_trx_hash_element_t(); +} + +/** Destructor callback for lock-free allocator. + +This destructor is used at shutdown. It frees remaining transaction objects. + +XA PREPARED transactions may remain if they haven't been committed or rolled +back. ACTIVE transactions may remain if startup was interrupted or server is +running in read-only mode or for certain srv_force_recovery levels. */ +void rw_trx_hash_t::rw_trx_hash_shutdown_destructor(uchar *arg) { + auto element = + reinterpret_cast(arg + LF_HASH_OVERHEAD); + trx_t *trx = element->trx; + if (trx != nullptr) { + ut_ad(trx_state_eq(trx, TRX_STATE_PREPARED) || + (trx_state_eq(trx, TRX_STATE_ACTIVE) && + (srv_is_being_started || srv_read_only_mode || + (srv_force_recovery >= SRV_FORCE_NO_TRX_UNDO)))); + trx_free_at_shutdown(trx); + } + element->~rw_trx_hash_element_t(); +} + +/** Initializer callback for lock-free hash. + +Object is not yet accessible via rw_trx_hash by concurrent threads, but is +about to become such. Object id can be changed only by this callback and +remains the same until all pins to this object are released. + +Object trx can be changed to 0 by erase() under object mutex protection, +which indicates it is about to be removed from lock-free hash and become not +accessible by concurrent threads. */ +void rw_trx_hash_t::rw_trx_hash_initialize(rw_trx_hash_element_t *element, + trx_t *trx) { + ut_ad(element->trx == nullptr); + element->trx = trx; + element->id = trx->id; + element->no = TRX_ID_MAX; + trx->rw_trx_hash_element = element; +} + +/** Gets LF_HASH pins. + +Pins are used to protect object from being destroyed or reused. They are +normally stored in trx object for quick access. If caller doesn't have trx +available, we try to get it using current_trx(). If caller doesn't have trx at +all, temporary pins are allocated. */ +LF_PINS *rw_trx_hash_t::get_pins(trx_t *trx) { + if (trx->rw_trx_hash_pins == nullptr) { + trx->rw_trx_hash_pins = lf_hash_get_pins(&hash); + ut_a(trx->rw_trx_hash_pins != nullptr); + } + + return trx->rw_trx_hash_pins; +} + +bool rw_trx_hash_t::eliminate_duplicates(rw_trx_hash_element_t *element, + eliminate_duplicates_arg *arg) { + for (auto id : arg->ids) { + if (id == element->id) { + return false; + } + } + + arg->ids.push_back(element->id); + return arg->action(element, arg->argument); +} + +#ifdef UNIV_DEBUG +void rw_trx_hash_t::validate_element(trx_t *trx) { + ut_ad(!trx->read_only || trx->rsegs.m_redo.rseg == nullptr); + ut_ad(!trx_is_autocommit_non_locking(trx)); + mutex_enter(&trx->mutex); + ut_ad(trx_state_eq(trx, TRX_STATE_ACTIVE) || + trx_state_eq(trx, TRX_STATE_PREPARED)); + mutex_exit(&trx->mutex); +} + +bool rw_trx_hash_t::debug_iterator(rw_trx_hash_element_t *element, + debug_iterator_arg *arg) { + mutex_enter(&element->mutex); + if (element->trx != nullptr) { + validate_element(element->trx); + } + mutex_exit(&element->mutex); + return arg->action(element, arg->argument); +} +#endif /* UNIV_DEBUG */ + +void rw_trx_hash_t::init() { + lf_hash_init(&hash, sizeof(rw_trx_hash_element_t), LF_HASH_UNIQUE, 0, + sizeof(trx_id_t), nullptr, &my_charset_bin); + hash.alloc.constructor = rw_trx_hash_constructor; + hash.alloc.destructor = rw_trx_hash_destructor; + hash.initialize = + reinterpret_cast(rw_trx_hash_initialize); +} + +void rw_trx_hash_t::destroy() { + hash.alloc.destructor = rw_trx_hash_shutdown_destructor; + lf_hash_destroy(&hash); +} + +/** Releases LF_HASH pins. + +Must be called by thread that owns trx_t object when the later is being +"detached" from thread (e.g. released to the pool by trx_free()). Can be +called earlier if thread is expected not to use rw_trx_hash. + +Since pins are not allowed to be transferred to another thread, +initialisation thread calls this for recovered transactions. */ +void rw_trx_hash_t::put_pins(trx_t *trx) { + if (trx->rw_trx_hash_pins != nullptr) { + lf_hash_put_pins(trx->rw_trx_hash_pins); + trx->rw_trx_hash_pins = nullptr; + } +} + +/** Finds trx object in lock-free hash with given id. + +Only ACTIVE or PREPARED trx objects may participate in hash. Nevertheless the +transaction may get committed before this method returns. + +With do_ref_count == false the caller may dereference returned trx pointer +only if lock_sys.mutex was acquired before calling find(). + +With do_ref_count == true caller dereferemce trx even if it is not holding +lock_sys.mutex. Caller is responsible for calling trx->release_reference() +when it is done playing with trx. + +Ideally this method should get caller rw_trx_hash_pins along with trx object +as a parameter, similar to insert() and erase(). However most callers lose trx +early in their call chains and it is not that easy to pass them through. + +So we take more expensive approach: get trx through current_thd()->ha_data. +Some threads don't have trx attached to THD, and at least server +initialisation thread, fts_optimize_thread, srv_master_thread, +dict_stats_thread, srv_monitor_thread, btr_defragment_thread don't even have +THD at all. For such cases we allocate pins only for duration of search and +free them immediately. + +This has negative performance impact and should be fixed eventually (by +passing caller_trx as a parameter). Still stream of DML is more or less Ok. + +@return pointer to trx or nullptr if not found */ +trx_t *rw_trx_hash_t::find(trx_t *caller_trx, trx_id_t trx_id, + bool do_ref_count) { + /* In Taurus, purge will reset DB_TRX_ID to 0 when the histroy is lost. + Read/Write transactions will always have a nonzero trx_t::id; there the + value 0 is reserved for transactions that did not write or lock anything + yet. + + The caller should already have handled trx_id==0 specially. */ + ut_ad(trx_id > 0); + if (caller_trx != nullptr && caller_trx->id == trx_id) { + if (do_ref_count) { + caller_trx->reference(); + } + return caller_trx; + } + + trx_t *trx = nullptr; + LF_PINS *pins = + (caller_trx != nullptr) ? get_pins(caller_trx) : lf_hash_get_pins(&hash); + ut_a(pins != nullptr); + + rw_trx_hash_element_t *element = reinterpret_cast( + lf_hash_search(&hash, pins, reinterpret_cast(&trx_id), + sizeof(trx_id_t))); + if (element != nullptr) { + mutex_enter(&element->mutex); + lf_hash_search_unpin(pins); + trx = element->trx; + if (trx == nullptr) { + } else if (UNIV_UNLIKELY(trx_id != trx->id)) { + trx = nullptr; + } else { + if (do_ref_count) { + trx->reference(); + } + ut_d(validate_element(trx)); + } + mutex_exit(&element->mutex); + } else { + lf_hash_search_unpin(pins); + } + if (caller_trx == nullptr) { + lf_hash_put_pins(pins); + } + return trx; +} + +/** Inserts trx to lock-free hash. + +Object becomes accessible via rw_trx_hash. */ +void rw_trx_hash_t::insert(trx_t *trx) { + ut_d(validate_element(trx)); + int res = lf_hash_insert(&hash, get_pins(trx), static_cast(trx)); + ut_a(res == 0); +} + +/** Removes trx from lock-free hash. + +Object becomes not accessible via rw_trx_hash. But it still can be pinned by +concurrent find(), which is supposed to release it immediately after it sees +object trx is nullptr. */ +void rw_trx_hash_t::erase(trx_t *trx) { + ut_d(validate_element(trx)); + mutex_enter(&trx->rw_trx_hash_element->mutex); + trx->rw_trx_hash_element->trx = nullptr; + mutex_exit(&trx->rw_trx_hash_element->mutex); + int res = + lf_hash_delete(&hash, get_pins(trx), static_cast(&trx->id), + sizeof(trx_id_t)); + ut_a(res == 0); +} + +/** Returns the number of elements in the hash. + +The number is exact only if hash is protected against concurrent modifications +(e.g., single threaded startup or hash is protected by some mutex). Otherwise +the number maybe used as a hint only, because it may change even before this +method returns. */ +uint32_t rw_trx_hash_t::size() { + return static_cast(hash.count.load(std::memory_order_relaxed)); +} + +/** Iterates the hash. + +@param caller_trx used to get/set pins +@param action called for every element in hash +@param argument opque argument passed to action + +May return the same element multiple times if hash is under contention. If +caller doesn't like to see the same transaction multiple times, it has to call +iterate_no_dups() instead. + +May return element with committed transaction. If caller doesn't like to see +committed transactions, it has to skip those under element mutex: + + mutex_enter(&element->mutex); + trx_t *trx = element->trx; + if (trx != nullptr) { + // trx is protected against commit in this branch + } + mutex_exit(&element->mutex); + +May miss concurrently inserted transactions. + +@return 0 if iteration completed successfuly, or 1 if iteration was +interrupted (action returned true) */ +int rw_trx_hash_t::iterate(trx_t *caller_trx, const lf_hash_walk_func *action, + const void *argument) { + LF_PINS *pins = + (caller_trx != nullptr) ? get_pins(caller_trx) : lf_hash_get_pins(&hash); + ut_a(pins != nullptr); +#ifdef UNIV_DEBUG + debug_iterator_arg debug_arg = {action, const_cast(argument)}; + action = reinterpret_cast(debug_iterator); + argument = &debug_arg; +#endif /* UNIV_DEBUG */ + int res = lf_hash_iterate(&hash, pins, action, argument); + if (caller_trx == nullptr) { + lf_hash_put_pins(pins); + } + return res; +} + +int rw_trx_hash_t::iterate(const lf_hash_walk_func *action, const void *argument) { + return iterate(current_trx(), action, argument); +} + +/** Iterates the hash and eliminates duplicate elements. + +@sa iterate() */ +int rw_trx_hash_t::iterate_no_dups(trx_t *caller_trx, lf_hash_walk_func *action, + void *argument) { + eliminate_duplicates_arg arg(size() + 32, action, argument); + return iterate(caller_trx, + reinterpret_cast(eliminate_duplicates), + &arg); +} + +int rw_trx_hash_t::iterate_no_dups(lf_hash_walk_func *action, void *argument) { + return iterate_no_dups(current_trx(), action, argument); +} + +/** Creates the trx_sys instance and initializes purge_queue and mutex. */ +void trx_sys_create() { + ut_ad(trx_sys == nullptr); + + trx_sys = static_cast(ut_zalloc_nokey(sizeof(*trx_sys))); + + mutex_create(LATCH_ID_TRX_SYS, &trx_sys->mutex); + + UT_LIST_INIT(trx_sys->trx_list, &trx_t::trx_list); + + new (&trx_sys->rsegs) Rsegs(); + trx_sys->rsegs.set_empty(); + + new (&trx_sys->tmp_rsegs) Rsegs(); + trx_sys->tmp_rsegs.set_empty(); + + trx_sys->rw_trx_hash.init(); +} +#else /** Creates the trx_sys instance and initializes purge_queue and mutex. */ void trx_sys_create(void) { ut_ad(trx_sys == NULL); @@ -528,6 +960,7 @@ void trx_sys_create(void) { new (&trx_sys->tmp_rsegs) Rsegs(); trx_sys->tmp_rsegs.set_empty(); } +#endif /* HAVE_LOCK_FREE_READVIEW */ /** Creates and initializes the transaction system at the database creation. */ void trx_sys_create_sys_pages(void) { @@ -540,6 +973,151 @@ void trx_sys_create_sys_pages(void) { mtr_commit(&mtr); } +#ifdef HAVE_LOCK_FREE_READVIEW +/** @return total number of active (non-prepared) transactions. */ +ulint trx_sys_t::any_active_transactions() { + ulint total_trx = 0; + + mutex_enter(&mutex); + for (trx_t *trx = UT_LIST_GET_FIRST(trx_list); trx != nullptr; + trx = UT_LIST_GET_NEXT(trx_list, trx)) { + if (trx->state == TRX_STATE_COMMITTED_IN_MEMORY || + (trx->state == TRX_STATE_ACTIVE && trx->id > 0)) { + total_trx++; + } + + if (srv_force_recovery >= SRV_FORCE_NO_TRX_UNDO) { + if (trx_state_eq(trx, TRX_STATE_ACTIVE) && trx->is_recovered) { + total_trx--; + } + } + } + mutex_exit(&mutex); + return total_trx; +} + +/** Clones the oldest view and stores it in view. + +No need to call ReadView::close(). The caller owns the view that is passed in. +This function is called by purge thread to determine whether it should purge the +delete marked record or not. */ +void trx_sys_t::clone_oldest_view(ReadView *view) { + if (view == NULL) { + purge_sys->view.snapshot(nullptr); + } + mutex_enter(&mutex); + /* Find oldest view. */ + for (const trx_t *trx = UT_LIST_GET_FIRST(trx_list); trx != nullptr; + trx = UT_LIST_GET_NEXT(trx_list, trx)) { + int32_t state; + while ((state = trx->read_view.get_state()) == READ_VIEW_STATE_SNAPSHOT) { + ut_delay(1); + } + + if (state == READ_VIEW_STATE_OPEN) { + if (view == NULL) { + purge_sys->view.copy(trx->read_view); + } else { + view->copy(trx->read_view); + } + } + } + mutex_exit(&mutex); + /* Update view to block purging transaction till GTID is persisted. */ + auto >id_persistor = clone_sys->get_gtid_persistor(); + auto gtid_oldest_trxno = gtid_persistor.get_oldest_trx_no(); + purge_sys->view.reduce_low_limit(gtid_oldest_trxno); +} + +struct trx_sys_found_prepared_trx_callback_arg { + uint count; +}; + +static bool trx_sys_found_prepared_trx_callback( + rw_trx_hash_element_t *element, + trx_sys_found_prepared_trx_callback_arg *arg) { + mutex_enter(&element->mutex); + trx_t *trx = element->trx; + if (trx != nullptr) { + if (trx_state_eq(trx, TRX_STATE_PREPARED)) { + arg->count++; + } + } + mutex_exit(&element->mutex); + return false; +} + +/** @return true if found prepared transaction(s). */ +bool trx_sys_t::found_prepared_trx() { + trx_sys_found_prepared_trx_callback_arg arg = {0}; + + trx_sys->rw_trx_hash.iterate(reinterpret_cast( + trx_sys_found_prepared_trx_callback), + &arg); + + return (arg.count > 0); +} + +trx_id_t trx_sys_t::get_new_trx_id_no_refresh() { + /* TODO wcy: why is this function called when doing undo? */ + + /* VERY important: after the database is started, max_trx_id value is + divisible by TRX_SYS_TRX_ID_WRITE_MARGIN, and the following if + will evaluate to TRUE when this function is first time called, + and the value for trx id will be written to disk-based header! + Thus trx id values will not overlap when the database is + repeatedly started! */ + + if (get_max_trx_id() % TRX_SYS_TRX_ID_WRITE_MARGIN == 0) { + trx_sys_flush_max_trx_id(); + } + + return max_trx_id.fetch_add(1, std::memory_order_relaxed); +} + +/********************************************************************* +Shutdown/Close the transaction system. */ +void trx_sys_close() { + ut_ad(srv_shutdown_state == SRV_SHUTDOWN_EXIT_THREADS); + + if (trx_sys == nullptr) { + return; + } + + ulint size = trx_sys->view_count(); + + if (size > 0) { + ib::error(ER_IB_MSG_1201) << "All read views were not closed before" + " shutdown: " + << size << " read views open"; + } + + if (trx_dummy_sess != nullptr) { + sess_close(trx_dummy_sess); + trx_dummy_sess = nullptr; + } + + trx_purge_sys_close(); + + /* Free the double write data structures. */ + buf_dblwr_free(); + + trx_sys->rw_trx_hash.destroy(); + + /* There can't be any active transactions. */ + trx_sys->rsegs.~Rsegs(); + + trx_sys->tmp_rsegs.~Rsegs(); + + ut_a(UT_LIST_GET_LEN(trx_sys->trx_list) == 0); + + /* We used placement new to create this mutex. Call the destructor. */ + mutex_free(&trx_sys->mutex); + + ut_free(trx_sys); + trx_sys = nullptr; +} +#else /********************************************************************* Shutdown/Close the transaction system. */ void trx_sys_close(void) { @@ -690,6 +1268,7 @@ bool trx_sys_validate_trx_list() { return (true); } #endif /* UNIV_DEBUG */ +#endif /* HAVE_LOCK_FREE_READVIEW */ #endif /* !UNIV_HOTBACKUP */ /** A list of undo tablespace IDs found in the TRX_SYS page. These are the diff --git a/storage/innobase/trx/trx0trx.cc b/storage/innobase/trx/trx0trx.cc index 4af1f5d..b2ce5af 100644 --- a/storage/innobase/trx/trx0trx.cc +++ b/storage/innobase/trx/trx0trx.cc @@ -110,7 +110,11 @@ committed. list of things we need to clean up during trx_erase_lists. */ +#ifdef HAVE_LOCK_FREE_READVIEW +static void trx_release_impl_and_expl_locks(trx_t *trx, bool atShutdown = false); +#else static void trx_release_impl_and_expl_locks(trx_t *trx, bool serialized); +#endif /** Set flush observer for the transaction @param[in,out] trx transaction struct @@ -205,7 +209,11 @@ static void trx_init(trx_t *trx) { trx->last_sql_stat_start.least_undo_no = 0; +#ifdef HAVE_LOCK_FREE_READVIEW + ut_ad(!trx->read_view.is_open()); +#else ut_ad(!MVCC::is_view_active(trx->read_view)); +#endif /* HAVE_LOCK_FREE_READVIEW */ trx->lock.rec_cached = 0; @@ -252,6 +260,13 @@ struct TrxFactory { new (&trx->lock.table_locks) lock_pool_t(); +#ifdef HAVE_LOCK_FREE_READVIEW + new (&trx->read_view) ReadView(); + + trx->rw_trx_hash_element = nullptr; + trx->rw_trx_hash_pins = nullptr; +#endif /* HAVE_LOCK_FREE_READVIEW */ + trx_init(trx); trx->state = TRX_STATE_NOT_STARTED; @@ -279,8 +294,10 @@ struct TrxFactory { @param trx the transaction for which to release resources */ static void destroy(trx_t *trx) { ut_a(trx->magic_n == TRX_MAGIC_N); +#ifndef HAVE_LOCK_FREE_READVIEW ut_ad(!trx->in_rw_trx_list); ut_ad(!trx->in_mysql_trx_list); +#endif /* HAVE_LOCK_FREE_READVIEW */ ut_a(trx->lock.wait_lock == NULL); ut_a(trx->lock.wait_thr == NULL); @@ -305,7 +322,12 @@ struct TrxFactory { trx->mod_tables.~trx_mod_tables_t(); +#ifdef HAVE_LOCK_FREE_READVIEW + ut_ad(!trx->read_view.is_open()); + trx->read_view.~ReadView(); +#else ut_ad(trx->read_view == NULL); +#endif /* HAVE_LOCK_FREE_READVIEW */ if (!trx->lock.rec_pool.empty()) { /* See lock_trx_alloc_locks() why we only free @@ -345,8 +367,10 @@ struct TrxFactory { ut_ad(trx->mysql_thd == 0); +#ifndef HAVE_LOCK_FREE_READVIEW ut_ad(!trx->in_rw_trx_list); ut_ad(!trx->in_mysql_trx_list); +#endif /* HAVE_LOCK_FREE_READVIEW */ ut_a(trx->lock.wait_thr == NULL); ut_a(trx->lock.wait_lock == NULL); @@ -448,6 +472,11 @@ static trx_t *trx_create_low() { /* We just got trx from pool, it should be non locking */ ut_ad(trx->will_lock == 0); +#ifdef HAVE_LOCK_FREE_READVIEW + ut_ad(trx->state == TRX_STATE_NOT_STARTED); + ut_ad(trx->rw_trx_hash_pins == nullptr); +#endif /* HAVE_LOCK_FREE_READVIEW */ + trx->persists_gtid = false; trx->api_trx = false; @@ -475,12 +504,77 @@ static trx_t *trx_create_low() { trx_free(). */ ut_a(trx->mod_tables.size() == 0); +#ifdef HAVE_LOCK_FREE_READVIEW + trx_sys->register_trx(trx); +#endif /* HAVE_LOCK_FREE_READVIEW */ return (trx); } /** Release a trx_t instance back to the pool. @param trx the instance to release. */ +#ifdef HAVE_LOCK_FREE_READVIEW +void trx_free(trx_t *&trx) { + ut_ad(!trx->declared_to_be_inside_innodb); + ut_ad(trx->n_mysql_tables_in_use == 0); + ut_ad(trx->mysql_n_tables_locked == 0); + ut_ad(!trx->internal); + + if (trx->declared_to_be_inside_innodb) { + ib::error(ER_IB_MSG_1270) + << "Freeing a trx (" << trx << ", " << trx_get_id_for_print(trx) + << ") which is declared" + " to be processing inside InnoDB"; + + trx_print(stderr, trx, 600); + putc('\n', stderr); + + /* This is an error but not a fatal error. We must keep the coutners like + srv_conc.n_active accurate. */ + srv_conc_force_exit_innodb(trx); + } + + if (trx->n_mysql_tables_in_use > 0 || trx->mysql_n_tables_locked > 0) { + ib::error(ER_IB_MSG_1270) + << "MySQL is freeing a thd though" + " trx->n_mysql_tables_in_use is " + << trx->n_mysql_tables_in_use << " and trx->mysql_n_tables_locked is " + << trx->mysql_n_tables_locked << "."; + + trx_print(stderr, trx, 600); + ut_print_buf(stderr, trx, sizeof(trx_t)); + putc('\n', stderr); + } + + trx->dict_operation = TRX_DICT_OP_NONE; + assert_trx_is_inactive(trx); + + trx_sys->deregister_trx(trx); + assert_trx_is_free(trx); + + trx_sys->rw_trx_hash.put_pins(trx); + trx->mysql_thd = nullptr; + + // FIXME: We need to avoid this heap free/alloc for each commit. + if (trx->lock.autoinc_locks != nullptr) { + ut_ad(ib_vector_is_empty(trx->lock.autoinc_locks)); + /* We allocated a dedicated heap for the vector. */ + ib_vector_free(trx->lock.autoinc_locks); + trx->lock.autoinc_locks = nullptr; + } + + trx->mod_tables.clear(); + + ut_ad(trx->is_dd_trx == false); + + /* trx locking state should have been reset before returning trx + to pool */ + ut_ad(trx->will_lock == 0); + + trx_pools->mem_free(trx); + trx = nullptr; +} +#else static void trx_free(trx_t *&trx) { assert_trx_is_free(trx); @@ -507,6 +601,7 @@ static void trx_free(trx_t *&trx) { trx = NULL; } +#endif /* HAVE_LOCK_FREE_READVIEW */ /** Creates a transaction object for background operations by the master thread. @return own: transaction object */ @@ -527,12 +622,14 @@ trx_t *trx_allocate_for_mysql(void) { trx = trx_allocate_for_background(); +#ifndef HAVE_LOCK_FREE_READVIEW trx_sys_mutex_enter(); ut_d(trx->in_mysql_trx_list = TRUE); UT_LIST_ADD_FIRST(trx_sys->mysql_trx_list, trx); trx_sys_mutex_exit(); +#endif /* HAVE_LOCK_FREE_READVIEW */ return (trx); } @@ -570,6 +667,7 @@ static void trx_validate_state_before_free(trx_t *trx) { assert_trx_is_inactive(trx); } +#ifndef HAVE_LOCK_FREE_READVIEW /** Free and initialize a transaction object instantiated during recovery. @param[in,out] trx transaction object to free and initialize */ void trx_free_resurrected(trx_t *trx) { @@ -579,6 +677,7 @@ void trx_free_resurrected(trx_t *trx) { trx_free(trx); } +#endif /* HAVE_LOCK_FREE_READVIEW */ /** Free a transaction that was allocated by background or user threads. @param[in,out] trx transaction object to free */ @@ -588,6 +687,32 @@ void trx_free_for_background(trx_t *trx) { trx_free(trx); } +#ifdef HAVE_LOCK_FREE_READVIEW +/** At shutdown, frees a transaction object. */ +void trx_free_at_shutdown(trx_t *trx) { + ut_ad(trx->is_recovered); + ut_a(trx_state_eq(trx, TRX_STATE_PREPARED) || + trx_state_eq(trx, TRX_STATE_ACTIVE)); + ut_a(trx->magic_n == TRX_MAGIC_N); + + trx_release_impl_and_expl_locks(trx, true); + trx_undo_free_at_shutdown(trx); + + ut_a(!trx->read_only); + + DBUG_LOG("trx", "Free prepared: " << trx); + trx->state = TRX_STATE_NOT_STARTED; + + /* Undo trx_resurrect_table_locks(). */ + lock_trx_lock_list_init(&trx->lock.trx_locks); + + /* Note: This vector is not guaranteed to be empty because the transaction + was never committed and therefore lock_trx_release() was not called. */ + trx->lock.table_locks.clear(); + + trx_free(trx); +} +#else /** At shutdown, frees a transaction object that is in the PREPARED state. */ void trx_free_prepared(trx_t *trx) /*!< in, own: trx object */ { @@ -609,6 +734,7 @@ void trx_free_prepared(trx_t *trx) /*!< in, own: trx object */ trx_free(trx); } +#endif /* HAVE_LOCK_FREE_READVIEW */ /** Disconnect a transaction from MySQL and optionally mark it as if it's been recovered. For the marking the transaction must be in prepared state. @@ -619,6 +745,18 @@ finally freed. @param[in] prepared boolean value to specify whether trx is for recovery or not. */ inline void trx_disconnect_from_mysql(trx_t *trx, bool prepared) { +#ifdef HAVE_LOCK_FREE_READVIEW + trx->read_view.close(); + + if (prepared) { + ut_ad(trx_state_eq(trx, TRX_STATE_PREPARED)); + + trx->is_recovered = true; + trx->mysql_thd = nullptr; + /* todo/fixme: suggest to do it at innodb prepare */ + trx->will_lock = 0; + } +#else trx_sys_mutex_enter(); ut_ad(trx->in_mysql_trx_list); @@ -642,6 +780,7 @@ inline void trx_disconnect_from_mysql(trx_t *trx, bool prepared) { } trx_sys_mutex_exit(); +#endif /* HAVE_LOCK_FREE_READVIEW */ } /** Disconnect a transaction from MySQL. @@ -793,7 +932,9 @@ static trx_t *trx_resurrect_insert( if (srv_force_recovery == 0) { trx->state = TRX_STATE_PREPARED; +#ifndef HAVE_LOCK_FREE_READVIEW ++trx_sys->n_prepared_trx; +#endif /* HAVE_LOCK_FREE_READVIEW */ } else { ib::info(ER_IB_MSG_1205) << "Since innodb_force_recovery" " > 0, we will force a rollback."; @@ -857,7 +998,9 @@ static void trx_resurrect_update_in_prepared_state( ut_ad(trx->state != TRX_STATE_FORCED_ROLLBACK); if (trx_state_eq(trx, TRX_STATE_NOT_STARTED)) { +#ifndef HAVE_LOCK_FREE_READVIEW ++trx_sys->n_prepared_trx; +#endif /* HAVE_LOCK_FREE_READVIEW */ } else { ut_ad(trx_state_eq(trx, TRX_STATE_PREPARED)); } @@ -950,7 +1093,17 @@ static void trx_resurrect(trx_rseg_t *rseg) { undo = UT_LIST_GET_NEXT(undo_list, undo)) { trx = trx_resurrect_insert(undo, rseg); +#ifdef HAVE_LOCK_FREE_READVIEW + trx_sys->rw_trx_hash.insert(trx); + trx_sys->rw_trx_hash.put_pins(trx); +#ifdef UNIV_DEBUG + if (trx->id > trx_sys->rw_max_trx_no) { + trx_sys->rw_max_trx_no = trx->id; + } +#endif /* UNIV_DEBUG */ +#else trx_sys_rw_trx_add(trx); +#endif /* HAVE_LOCK_FREE_READVIEW */ trx_resurrect_table_ids(trx, &trx->rsegs.m_redo, undo); } @@ -958,6 +1111,30 @@ static void trx_resurrect(trx_rseg_t *rseg) { /* Ressurrect transactions that were doing updates. */ for (undo = UT_LIST_GET_FIRST(rseg->update_undo_list); undo != NULL; undo = UT_LIST_GET_NEXT(undo_list, undo)) { +#ifdef HAVE_LOCK_FREE_READVIEW + /* Check the trx_sys->rw_trx_hash first. */ + trx_t *trx = trx_sys->find(nullptr, undo->trx_id, false); + + if (trx == nullptr) { + trx = trx_allocate_for_background(); + + ut_d(trx->start_file = __FILE__); + ut_d(trx->start_line = __LINE__); + + trx_resurrect_update(trx, undo, rseg); + + trx_sys->rw_trx_hash.insert(trx); + trx_sys->rw_trx_hash.put_pins(trx); + } else { + trx_resurrect_update(trx, undo, rseg); + } + +#ifdef UNIV_DEBUG + if (trx->id > trx_sys->rw_max_trx_no) { + trx_sys->rw_max_trx_no = trx->id; + } +#endif /* UNIV_DEBUG */ +#else /* Check the trx_sys->rw_trx_set first. */ trx_sys_mutex_enter(); @@ -975,6 +1152,7 @@ static void trx_resurrect(trx_rseg_t *rseg) { trx_resurrect_update(trx, undo, rseg); trx_sys_rw_trx_add(trx); +#endif /* HAVE_LOCK_FREE_READVIEW */ trx_resurrect_table_ids(trx, &trx->rsegs.m_redo, undo); } @@ -1006,6 +1184,7 @@ void trx_lists_init_at_db_start(void) { } undo::spaces->s_unlock(); +#ifndef HAVE_LOCK_FREE_READVIEW TrxIdSet::iterator end = trx_sys->rw_trx_set.end(); for (TrxIdSet::iterator it = trx_sys->rw_trx_set.begin(); it != end; ++it) { @@ -1018,6 +1197,7 @@ void trx_lists_init_at_db_start(void) { UT_LIST_ADD_FIRST(trx_sys->rw_trx_list, it->m_trx); } +#endif /* HAVE_LOCK_FREE_READVIEW */ } /** Get next redo rollback segment in round-robin fashion. @@ -1185,6 +1365,9 @@ void trx_assign_rseg_temp(trx_t *trx) { srv_read_only_mode ? nullptr : get_next_temp_rseg(); if (trx->id == 0) { +#ifdef HAVE_LOCK_FREE_READVIEW + trx_sys->register_rw(trx); +#else mutex_enter(&trx_sys->mutex); trx->id = trx_sys_get_new_trx_id(); @@ -1194,6 +1377,7 @@ void trx_assign_rseg_temp(trx_t *trx) { trx_sys->rw_trx_set.insert(TrxTrack(trx->id, trx)); mutex_exit(&trx_sys->mutex); +#endif /* HAVE_LOCK_FREE_READVIEW */ } } @@ -1256,6 +1440,16 @@ static void trx_start_low( ut_a(ib_vector_is_empty(trx->lock.autoinc_locks)); ut_a(trx->lock.table_locks.empty()); +#ifdef HAVE_LOCK_FREE_READVIEW + /* No other thread can access this trx object through rw_trx_hash, thus we + don't need trx_sys.mutex protection for that purposes. Still this trx can be + found through trx_sys->trx_list, which means state change must be protected by + e.g. trx->mutex. + + For now we update it without mutex protection, because original code did it in + this way. It has to be reviewed and fixed properly. */ + trx->state = TRX_STATE_ACTIVE; +#else /* If this transaction came from trx_allocate_for_mysql(), trx->in_mysql_trx_list would hold. In that case, the trx->state change must be protected by the trx_sys->mutex, so that @@ -1267,6 +1461,7 @@ static void trx_start_low( e.g., the transaction state can be set earlier but we are forced to set it under the protection of the trx_sys_t::mutex because some trx list assertions are triggered unnecessarily. */ +#endif /* HAVE_LOCK_FREE_READVIEW */ /* By default all transactions are in the read-only list unless they are non-locking auto-commit read only transactions or background @@ -1281,6 +1476,17 @@ static void trx_start_low( /* Temporary rseg is assigned only if the transaction updates a temporary table */ +#ifdef HAVE_LOCK_FREE_READVIEW + ut_ad(trx->rsegs.m_redo.rseg != nullptr || srv_read_only_mode || + srv_force_recovery >= SRV_FORCE_NO_TRX_UNDO); + + trx_sys->register_rw(trx); +#ifdef UNIV_DEBUG + if (trx->id > trx_sys->rw_max_trx_no) { + trx_sys->rw_max_trx_no = trx->id; + } +#endif /* UNIV_DEBUG */ +#else trx_sys_mutex_enter(); trx->id = trx_sys_get_new_trx_id(); @@ -1301,6 +1507,7 @@ static void trx_start_low( ut_ad(trx_sys_validate_trx_list()); trx_sys_mutex_exit(); +#endif /* HAVE_LOCK_FREE_READVIEW */ } else { trx->id = 0; @@ -1311,6 +1518,10 @@ static void trx_start_low( to write to the temporary table. */ if (read_write) { +#ifdef HAVE_LOCK_FREE_READVIEW + ut_ad(!srv_read_only_mode); + trx_sys->register_rw(trx); +#else trx_sys_mutex_enter(); ut_ad(!srv_read_only_mode); @@ -1322,13 +1533,18 @@ static void trx_start_low( trx_sys->rw_trx_set.insert(TrxTrack(trx->id, trx)); trx_sys_mutex_exit(); +#endif /* HAVE_LOCK_FREE_READVIEW */ } +#ifndef HAVE_LOCK_FREE_READVIEW trx->state = TRX_STATE_ACTIVE; +#endif /* HAVE_LOCK_FREE_READVIEW */ } else { ut_ad(!read_write); +#ifndef HAVE_LOCK_FREE_READVIEW trx->state = TRX_STATE_ACTIVE; +#endif /* HAVE_LOCK_FREE_READVIEW */ } } @@ -1346,6 +1562,194 @@ static void trx_start_low( MONITOR_INC(MONITOR_TRX_ACTIVE); } +#ifdef HAVE_LOCK_FREE_READVIEW +/** Set the transaction serialisation number. + @return true if the transaction number was added to the serialisation_list. */ +static void trx_serialise( + trx_t *trx, /*!< in/out: transaction */ + mtr_t *mtr, /*!< in/out: mini-transaction */ + trx_undo_ptr_t *redo_rseg_undo_ptr, /*!< in/out: Set trx + serialisation number in + referred undo rseg. */ + trx_undo_ptr_t *temp_rseg_undo_ptr) /*!< in/out: Set trx + serialisation number in + referred undo rseg. */ +{ + trx_rseg_t *redo_rseg = nullptr; + trx_rseg_t *temp_rseg = nullptr; + //trx_id_t new_id; + + if (redo_rseg_undo_ptr != nullptr) { + ut_ad(mutex_own(&redo_rseg_undo_ptr->rseg->mutex)); + redo_rseg = redo_rseg_undo_ptr->rseg; + } + + if (temp_rseg_undo_ptr != nullptr) { + ut_ad(mutex_own(&temp_rseg_undo_ptr->rseg->mutex)); + temp_rseg = temp_rseg_undo_ptr->rseg; + } + + if ((redo_rseg != nullptr && redo_rseg->last_page_no == FIL_NULL) || + (temp_rseg != nullptr && temp_rseg->last_page_no == FIL_NULL)) { + /* NOTE: here we MUST acquire the mutex to protect the purge queue before + invoking assign_new_trx_no, otherwise it might be possible to push a + segment with smaller trx_no than last time popped. */ + mutex_enter(&purge_sys->pq_mutex); + } + + trx_sys->assign_new_trx_no(trx); + //new_id = trx->no; + + /* If the rollack segment is not empty then the + new trx_t::no can't be less than any trx_t::no + already in the rollback segment. User threads only + produce events when a rollback segment is empty. */ + if ((redo_rseg != nullptr && redo_rseg->last_page_no == FIL_NULL) || + (temp_rseg != nullptr && temp_rseg->last_page_no == FIL_NULL)) { + TrxUndoRsegs elem(trx->no); + + if (redo_rseg != nullptr && redo_rseg->last_page_no == FIL_NULL) { + elem.push_back(redo_rseg); + } + + if (temp_rseg != nullptr && temp_rseg->last_page_no == FIL_NULL) { + elem.push_back(temp_rseg); + } + + purge_sys->purge_queue->push(elem); + mutex_exit(&purge_sys->pq_mutex); + } + + /* new_id is a special trx id created for r/w trx which has update + undo. Such trx id is used by purge and the trx itself has no activity + so it's "effectived" committed right away. We need explicitly log it + in redo log, otherwise replica nodes would think this trx remains + active for ever. */ + //mlog_log_transaction_end(mtr, new_id); +} + +/** Assign the transaction its history serialisation number and write the + update UNDO log record to the assigned rollback segment. + @return true if a serialisation log was written */ +static void trx_write_serialisation_history( + trx_t *trx, /*!< in/out: transaction */ + mtr_t *mtr) /*!< in/out: mini-transaction */ +{ + /* Change the undo log segment states from TRX_UNDO_ACTIVE to some + other state: these modifications to the file data structure define + the transaction as committed in the file based domain, at the + serialization point of the log sequence number lsn obtained below. */ + + /* We have to hold the rseg mutex because update log headers have + to be put to the history list in the (serialisation) order of the + UNDO trx number. This is required for the purge in-memory data + structures too. */ + + bool own_redo_rseg_mutex = false; + bool own_temp_rseg_mutex = false; + + /* Get rollback segment mutex. */ + if (trx->rsegs.m_redo.rseg != nullptr && trx_is_redo_rseg_updated(trx)) { + mutex_enter(&trx->rsegs.m_redo.rseg->mutex); + own_redo_rseg_mutex = true; + } + + mtr_t temp_mtr; + + if (trx->rsegs.m_noredo.rseg != nullptr && trx_is_temp_rseg_updated(trx)) { + mutex_enter(&trx->rsegs.m_noredo.rseg->mutex); + own_temp_rseg_mutex = true; + mtr_start(&temp_mtr); + temp_mtr.set_log_mode(MTR_LOG_NO_REDO); + } + + /* If transaction involves insert then truncate undo logs. */ + if (trx->rsegs.m_redo.insert_undo != nullptr) { + trx_undo_set_state_at_finish(trx->rsegs.m_redo.insert_undo, mtr); + } + + if (trx->rsegs.m_noredo.insert_undo != nullptr) { + trx_undo_set_state_at_finish(trx->rsegs.m_noredo.insert_undo, &temp_mtr); + } + + /* If transaction involves update then add rollback segments + to purge queue. */ + if (trx->rsegs.m_redo.update_undo != nullptr || + trx->rsegs.m_noredo.update_undo != nullptr) { + /* Assign the transaction serialisation number and add these + rollback segments to purge trx-no sorted priority queue + if this is the first UNDO log being written to assigned + rollback segments. */ + + trx_undo_ptr_t *redo_rseg_undo_ptr = + trx->rsegs.m_redo.update_undo != nullptr ? &trx->rsegs.m_redo : nullptr; + + trx_undo_ptr_t *temp_rseg_undo_ptr = + trx->rsegs.m_noredo.update_undo != nullptr ? &trx->rsegs.m_noredo + : nullptr; + + /* Will set trx->no and will add rseg to purge queue. */ + trx_serialise(trx, mtr, redo_rseg_undo_ptr, temp_rseg_undo_ptr); + + /* It is not necessary to obtain trx->undo_mutex here because + only a single OS thread is allowed to do the transaction commit + for this transaction. */ + if (trx->rsegs.m_redo.update_undo != nullptr) { + page_t *undo_hdr_page; + + undo_hdr_page = + trx_undo_set_state_at_finish(trx->rsegs.m_redo.update_undo, mtr); + + /* Delay update of rseg_history_len if we plan to add + non-redo update_undo too. This is to avoid immediate + invocation of purge as we need to club these 2 segments + with same trx-no as single unit. */ + bool update_rseg_len = !(trx->rsegs.m_noredo.update_undo != nullptr); + + /* Set flag if GTID information need to persist. */ + auto undo_ptr = &trx->rsegs.m_redo; + trx_undo_gtid_set(trx, undo_ptr->update_undo); + + trx_undo_update_cleanup(trx, undo_ptr, undo_hdr_page, update_rseg_len, + (update_rseg_len ? 1 : 0), mtr); + } + + DBUG_EXECUTE_IF("ib_trx_crash_during_commit", DBUG_SUICIDE();); + + if (trx->rsegs.m_noredo.update_undo != nullptr) { + page_t *undo_hdr_page; + + undo_hdr_page = trx_undo_set_state_at_finish( + trx->rsegs.m_noredo.update_undo, &temp_mtr); + + ulint n_added_logs = (redo_rseg_undo_ptr != nullptr) ? 2 : 1; + + trx_undo_update_cleanup(trx, &trx->rsegs.m_noredo, undo_hdr_page, true, + n_added_logs, &temp_mtr); + } + } + + if (own_redo_rseg_mutex) { + mutex_exit(&trx->rsegs.m_redo.rseg->mutex); + own_redo_rseg_mutex = false; + } + + if (own_temp_rseg_mutex) { + mutex_exit(&trx->rsegs.m_noredo.rseg->mutex); + own_temp_rseg_mutex = false; + mtr_commit(&temp_mtr); + } + + MONITOR_INC(MONITOR_TRX_COMMIT_UNDO); + + /* Update the latest MySQL binlog name and offset information + in trx sys header only if MySQL binary logging is on and clone + is has ensured commit order at final stage. */ + if (Clone_handler::need_commit_order()) { // NOLINT + trx_sys_update_mysql_binlog_offset(trx, mtr); + } +} +#else /** Set the transaction serialisation number. @return true if the transaction number was added to the serialisation_list. */ static bool trx_serialisation_number_get( @@ -1546,6 +1950,7 @@ static bool trx_write_serialisation_history( return (serialised); } +#endif /* HAVE_LOCK_FREE_READVIEW */ /******************************************************************** Finalize a transaction containing updates for a FTS table. */ @@ -1687,10 +2092,239 @@ static void trx_update_mod_tables_timestamp(trx_t *trx) /*!< in: transaction */ trx->mod_tables.clear(); } -/** -Erase the transaction from running transaction lists and serialization -list. Active RW transaction list of a MVCC snapshot(ReadView::prepare) -won't include this transaction after this call. All implicit locks are +#ifdef HAVE_LOCK_FREE_READVIEW +static void trx_release_impl_and_expl_locks(trx_t *trx, bool atShutdown) { + check_trx_state(trx); + ut_ad(trx_state_eq(trx, TRX_STATE_ACTIVE) || + trx_state_eq(trx, TRX_STATE_PREPARED)); + + /* Check and get GTID to be persisted. Do it outside trx_sys mutex. */ + Gtid_desc gtid_desc; + auto >id_persistor = clone_sys->get_gtid_persistor(); + gtid_persistor.get_gtid_info(trx, gtid_desc); + + if (trx->id > 0) { + /* For consistent snapshot, we need to remove current + transaction from running transaction id list for mvcc + before doing commit and releasing locks. */ + if (!atShutdown) { + trx_sys->deregister_rw(trx); + } + + /* Add GTID to be persisted to disk table. It must be done ... + 1.After the transaction is marked committed in undo. Otherwise + GTID might get committed before the transaction commit on disk. + 2.Before it is removed from serialization list. Otherwise the transaction + undo could get purged before persisting GTID on disk table. */ + if (gtid_desc.m_is_set) { + auto >id_persistor = clone_sys->get_gtid_persistor(); + gtid_persistor.add(gtid_desc); + } + } + + trx_mutex_enter(trx); + /* Please consider this particular point in time as the moment the trx's + implicit locks become released. + This change is protected by both trx_sys->mutex and trx->mutex. + Therefore, there are two secure ways to check if the trx still can hold + implicit locks: + (1) if you only know id of the trx, then you can obtain trx_sys->mutex and + check if trx is still in rw_trx_set. This works, because the call to + trx_erase_list() which removes trx from this list several lines above is + also protected by trx_sys->mutex. We use this approach in + lock_rec_convert_impl_to_expl() by using trx_rw_is_active() + (2) if you have pointer to trx, and you know it is safe to access (say, you + hold reference to this trx which prevents it from being freed) then you + can obtain trx->mutex and check if trx->state is equal to + TRX_STATE_COMMITTED_IN_MEMORY. We use this approach in + lock_rec_convert_impl_to_expl_for_trx() when deciding for the final time + if we really want to create explicit lock on behalf of implicit lock + holder. */ + trx->state = TRX_STATE_COMMITTED_IN_MEMORY; + trx_mutex_exit(trx); + + lock_trx_release_locks(trx); +} + +/** Commits a transaction in memory. */ +static void trx_commit_in_memory( + trx_t *trx, /*!< in/out: transaction */ + const mtr_t *mtr) /*!< in: mini-transaction of + trx_write_serialisation_history(), or NULL if + the transaction did not modify anything */ +{ + trx->must_flush_log_later = false; + trx->ddl_must_flush = false; + trx->read_view.close(); + + if (trx_is_autocommit_non_locking(trx)) { + ut_ad(trx->id == 0); + ut_ad(trx->read_only); + ut_a(!trx->is_recovered); + ut_ad(trx->rsegs.m_redo.rseg == nullptr); + + /* Note: We are asserting without holding the lock mutex. But + that is OK because this transaction is not waiting and cannot + be rolled back and no new locks can (or should not) be added + because it is flagged as a non-locking read-only transaction. */ + + ut_a(UT_LIST_GET_LEN(trx->lock.trx_locks) == 0); + + /* This state change is not protected by any mutex, therefore + there is an inherent race here around state transition during + printouts. We ignore this race for the sake of efficiency. + However, the trx_sys_t::mutex will protect the trx_t instance + and it cannot be removed from the mysql_trx_list and freed + without first acquiring the trx_sys_t::mutex. */ + + ut_ad(trx_state_eq(trx, TRX_STATE_ACTIVE)); + + MONITOR_INC(MONITOR_TRX_NL_RO_COMMIT); + + /* AC-NL-RO transactions can't be rolled back asynchronously. */ + ut_ad(!trx->abort); + ut_ad(!(trx->in_innodb & (TRX_FORCE_ROLLBACK | TRX_FORCE_ROLLBACK_ASYNC))); + + trx->state = TRX_STATE_NOT_STARTED; + + } else { + trx_release_impl_and_expl_locks(trx); + DEBUG_SYNC_C("after_remove_serial_list_release_locks"); + + /* Remove the transaction from the list of active + transactions now that it no longer holds any user locks. */ + + ut_ad(trx_state_eq(trx, TRX_STATE_COMMITTED_IN_MEMORY)); + DEBUG_SYNC_C("after_trx_committed_in_memory"); + + if (trx->read_only || trx->rsegs.m_redo.rseg == nullptr) { + MONITOR_INC(MONITOR_TRX_RO_COMMIT); + } else { + ut_ad(trx->id > 0); + MONITOR_INC(MONITOR_TRX_RW_COMMIT); + } + } + + if (trx->rsegs.m_redo.rseg != nullptr) { + trx_rseg_t *rseg = trx->rsegs.m_redo.rseg; + ut_ad(rseg->trx_ref_count > 0); + + /* Multiple transactions can simultaneously decrement + the atomic counter. */ + rseg->trx_ref_count--; + } + + /* Reset flag that SE persists GTID. */ + auto >id_persistor = clone_sys->get_gtid_persistor(); + gtid_persistor.set_persist_gtid(trx, false); + + if (mtr != nullptr) { + if (trx->rsegs.m_redo.insert_undo != nullptr) { + trx_undo_insert_cleanup(&trx->rsegs.m_redo, false); + } + + if (trx->rsegs.m_noredo.insert_undo != nullptr) { + trx_undo_insert_cleanup(&trx->rsegs.m_noredo, true); + } + + /* NOTE that we could possibly make a group commit more + efficient here: call os_thread_yield here to allow also other + trxs to come to commit! */ + + /*-------------------------------------*/ + + /* Depending on the my.cnf options, we may now write the log + buffer to the log files, making the transaction durable if + the OS does not crash. We may also flush the log files to + disk, making the transaction durable also at an OS crash or a + power outage. + + The idea in InnoDB's group commit is that a group of + transactions gather behind a trx doing a physical disk write + to log files, and when that physical write has been completed, + one of those transactions does a write which commits the whole + group. Note that this group commit will only bring benefit if + there are > 2 users in the database. Then at least 2 users can + gather behind one doing the physical log write to disk. + + If we are calling trx_commit() under prepare_commit_mutex, we + will delay possible log write and flush to a separate function + trx_commit_complete_for_mysql(), which is only called when the + thread has released the mutex. This is to make the + group commit algorithm to work. Otherwise, the prepare_commit + mutex would serialize all commits and prevent a group of + transactions from gathering. */ + + lsn_t lsn = mtr->commit_lsn(); + + if (lsn == 0) { + /* Nothing to be done. */ + } else if (trx->flush_log_later) { + /* Do nothing yet */ + trx->must_flush_log_later = true; + + /* Remember current ddl_operation, because trx_init() + later will set ddl_operation to false. And the final + flush is even later. */ + trx->ddl_must_flush = trx->ddl_operation; + } else if ((srv_flush_log_at_trx_commit == 0 || + thd_requested_durability(trx->mysql_thd) == + HA_IGNORE_DURABILITY) && + (!trx->ddl_operation)) { + /* Do nothing */ + } else { + trx_flush_log_if_needed(lsn, trx); + } + + trx->commit_lsn = lsn; + + /* Tell server some activity has happened, since the trx + does changes something. Background utility threads like + master thread, purge thread or page_cleaner thread might + have some work to do. */ + srv_active_wake_master_thread(); + } + + /* Free all savepoints, starting from the first. */ + trx_named_savept_t *savep = UT_LIST_GET_FIRST(trx->trx_savepoints); + + trx_roll_savepoints_free(trx, savep); + + if (trx->fts_trx != nullptr) { + trx_finalize_for_fts(trx, trx->undo_no != 0); + } + + trx_mutex_enter(trx); + trx->dict_operation = TRX_DICT_OP_NONE; + + /* Because we can rollback transactions asynchronously, we change + the state at the last step. trx_t::abort cannot change once commit + or rollback has started because we will have released the locks by + the time we get here. */ + + if (trx->abort) { + trx->abort = false; + trx->state = TRX_STATE_FORCED_ROLLBACK; + } else { + trx->state = TRX_STATE_NOT_STARTED; + } + + /* trx->in_mysql_trx_list would hold between + trx_allocate_for_mysql() and trx_free_for_mysql(). It does not + hold for recovered transactions or system transactions. */ + assert_trx_is_free(trx); + + trx_init(trx); + + trx_mutex_exit(trx); + + ut_a(trx->error_state == DB_SUCCESS); +} +#else +/** +Erase the transaction from running transaction lists and serialization +list. Active RW transaction list of a MVCC snapshot(ReadView::prepare) +won't include this transaction after this call. All implicit locks are also released by this call as trx is removed from rw_trx_list. @param[in] trx Transaction to erase, must have an ID > 0 @param[in] serialised true if serialisation log was written @@ -1980,6 +2614,7 @@ written */ ut_a(trx->error_state == DB_SUCCESS); } +#endif /* HAVE_LOCK_FREE_READVIEW */ /** Commits a transaction and a mini-transaction. */ void trx_commit_low( @@ -2013,12 +2648,18 @@ void trx_commit_low( } } +#ifndef HAVE_LOCK_FREE_READVIEW bool serialised; +#endif /* HAVE_LOCK_FREE_READVIEW */ if (mtr != NULL) { mtr->set_sync(); +#ifdef HAVE_LOCK_FREE_READVIEW + trx_write_serialisation_history(trx, mtr); +#else serialised = trx_write_serialisation_history(trx, mtr); +#endif /* HAVE_LOCK_FREE_READVIEW */ /* The following call commits the mini-transaction, making the whole transaction committed in the file-based world, at this @@ -2056,7 +2697,9 @@ void trx_commit_low( /*--------------*/ } else { +#ifndef HAVE_LOCK_FREE_READVIEW serialised = false; +#endif /* HAVE_LOCK_FREE_READVIEW */ } #ifdef UNIV_DEBUG /* In case of this function is called from a stack executing @@ -2072,7 +2715,11 @@ void trx_commit_low( } #endif +#ifdef HAVE_LOCK_FREE_READVIEW + trx_commit_in_memory(trx, mtr); +#else trx_commit_in_memory(trx, mtr, serialised); +#endif /* HAVE_LOCK_FREE_READVIEW */ } /** Commits a transaction. */ @@ -2098,6 +2745,7 @@ void trx_commit(trx_t *trx) /*!< in/out: transaction */ trx_commit_low(trx, mtr); } +#ifndef HAVE_LOCK_FREE_READVIEW /** Cleans up a transaction at database startup. The cleanup is needed if the transaction already got to the middle of a commit when the database crashed, and we cannot roll it back. */ @@ -2155,6 +2803,7 @@ ReadView *trx_assign_read_view(trx_t *trx) /*!< in/out: active transaction */ return (trx->read_view); } +#endif /* HAVE_LOCK_FREE_READVIEW */ /** Prepares a transaction for commit/rollback. */ void trx_commit_or_rollback_prepare(trx_t *trx) /*!< in/out: transaction */ @@ -2377,7 +3026,9 @@ void trx_print_low(FILE *f, ibool newline; const char *op_info; +#ifndef HAVE_LOCK_FREE_READVIEW ut_ad(trx_sys_mutex_own()); +#endif /* HAVE_LOCK_FREE_READVIEW */ fprintf(f, "TRANSACTION " TRX_ID_FMT, trx_get_id_for_print(trx)); @@ -2494,7 +3145,9 @@ void trx_print_latched( or 0 to use the default max length */ { ut_ad(lock_mutex_own()); +#ifndef HAVE_LOCK_FREE_READVIEW ut_ad(trx_sys_mutex_own()); +#endif /* HAVE_LOCK_FREE_READVIEW */ trx_print_low(f, trx, max_query_len, lock_number_of_rows_locked(&trx->lock), UT_LIST_GET_LEN(trx->lock.trx_locks), @@ -2518,11 +3171,15 @@ void trx_print(FILE *f, /*!< in: output stream */ heap_size = mem_heap_get_size(trx->lock.lock_heap); lock_mutex_exit(); +#ifndef HAVE_LOCK_FREE_READVIEW mutex_enter(&trx_sys->mutex); +#endif /* HAVE_LOCK_FREE_READVIEW */ trx_print_low(f, trx, max_query_len, n_rec_locks, n_trx_locks, heap_size); +#ifndef HAVE_LOCK_FREE_READVIEW mutex_exit(&trx_sys->mutex); +#endif /* HAVE_LOCK_FREE_READVIEW */ } #ifdef UNIV_DEBUG @@ -2531,7 +3188,9 @@ void trx_print(FILE *f, /*!< in: output stream */ @return true if started */ ibool trx_assert_started(const trx_t *trx) /*!< in: transaction */ { +#ifndef HAVE_LOCK_FREE_READVIEW ut_ad(trx_sys_mutex_own()); +#endif /* HAVE_LOCK_FREE_READVIEW */ /* Non-locking autocommits should not hold any locks and this function is only called from the locking code. */ @@ -2681,6 +3340,15 @@ static void trx_prepare(trx_t *trx) /*!< in/out: transaction */ /*--------------------------------------*/ ut_a(trx->state == TRX_STATE_ACTIVE); +#ifdef HAVE_LOCK_FREE_READVIEW + trx_mutex_enter(trx); + trx->state = TRX_STATE_PREPARED; + /* Add GTID to be persisted to disk table, if needed. */ + if (gtid_desc.m_is_set) { + gtid_persistor.add(gtid_desc); + } + trx_mutex_exit(trx); +#else trx_sys_mutex_enter(); trx->state = TRX_STATE_PREPARED; trx_sys->n_prepared_trx++; @@ -2689,6 +3357,7 @@ static void trx_prepare(trx_t *trx) /*!< in/out: transaction */ gtid_persistor.add(gtid_desc); } trx_sys_mutex_exit(); +#endif /* HAVE_LOCK_FREE_READVIEW */ /*--------------------------------------*/ DEBUG_SYNC_C("trx_prepare_has_changed_state"); @@ -2830,6 +3499,123 @@ static bool get_info_about_prepared_transaction(XA_recover_txn *txn_list, return false; } +#ifdef HAVE_LOCK_FREE_READVIEW +struct trx_recover_for_mysql_callback_arg { + XA_recover_txn *txn_list; + MEM_ROOT *mem_root; + ulint len; + ulint count; +}; + +static bool trx_recover_for_mysql_callback( + rw_trx_hash_element_t *element, trx_recover_for_mysql_callback_arg *arg) { + mutex_enter(&element->mutex); + trx_t *trx = element->trx; + if (trx != nullptr) { + /* The state of a read-write transaction cannot change + from or to NOT_STARTED while we are holding the + trx_sys->mutex. It may change to PREPARED, but not if + trx->is_recovered. It may also change to COMMITTED. */ + if (trx_state_eq(trx, TRX_STATE_PREPARED)) { + if (get_info_about_prepared_transaction(&arg->txn_list[arg->count], trx, + arg->mem_root)) { + mutex_exit(&element->mutex); + return true; + } + + if (arg->count == 0) { + ib::info(ER_IB_MSG_1207) << "Starting recovery for" + " XA transactions..."; + } + + ib::info(ER_IB_MSG_1208) << "Transaction " << trx_get_id_for_print(trx) + << " in prepared state after recovery"; + + ib::info(ER_IB_MSG_1209) + << "Transaction contains changes to " << trx->undo_no << " rows"; + + arg->txn_list[arg->count++].id = *trx->xid; + } + } + mutex_exit(&element->mutex); + return (arg->count == arg->len); +} + +/** This function is used to find number of prepared transactions and + their transaction objects for a recovery. + @return number of prepared transactions stored in xid_list */ +int trx_recover_for_mysql( + XA_recover_txn *txn_list, /*!< in/out: prepared transactions */ + ulint len, /*!< in: number of slots in xid_list */ + MEM_ROOT *mem_root) /*!< in: memory for table names */ +{ + ut_ad(txn_list != nullptr); + ut_ad(len > 0); + + trx_recover_for_mysql_callback_arg arg = {txn_list, mem_root, len, 0U}; + + /* Fill txb_list with PREPARED transactions. */ + trx_sys->rw_trx_hash.iterate_no_dups( + reinterpret_cast(trx_recover_for_mysql_callback), + &arg); + + if (arg.count > 0) { + ib::info(ER_IB_MSG_1210) << arg.count + << " transactions in prepared state" + " after recovery"; + } + + return (static_cast(arg.count)); +} + +struct trx_get_trx_by_xid_callback_arg { + const XID *xid; + trx_t *trx; +}; + +static bool trx_get_trx_by_xid_callback(rw_trx_hash_element_t *element, + trx_get_trx_by_xid_callback_arg *arg) { + bool found = false; + + mutex_enter(&element->mutex); + trx_t *trx = element->trx; + if (trx != nullptr) { + /* Compare two X/Open XA transaction id's: their + length should be the same and binary comparison + of gtrid_length+bqual_length bytes should be + the same */ + + if (trx->is_recovered && trx_state_eq(trx, TRX_STATE_PREPARED) && + arg->xid->eq(trx->xid)) { + /* Invalidate the XID, so that subsequent calls + will not find it. */ + trx->xid->reset(); + arg->trx = trx; + found = true; + } + } + mutex_exit(&element->mutex); + + return found; +} + +/** This function is used to find one X/Open XA distributed transaction + which is in the prepared state + @return trx or NULL; on match, the trx->xid will be invalidated; + note that the trx may have been committed, unless the caller is + holding lock_sys->mutex */ +trx_t *trx_get_trx_by_xid( + const XID *xid) /*!< in: X/Open XA transaction identifier */ +{ + trx_get_trx_by_xid_callback_arg arg = {xid, nullptr}; + if (xid != nullptr) { + trx_sys->rw_trx_hash.iterate( + reinterpret_cast(trx_get_trx_by_xid_callback), + &arg); + } + return arg.trx; +} +#else /** This function is used to find number of prepared transactions and their transaction objects for a recovery. @return number of prepared transactions stored in xid_list */ @@ -2949,6 +3735,7 @@ trx_t *trx_get_trx_by_xid( return (trx); } +#endif /* HAVE_LOCK_FREE_READVIEW */ /** Starts the transaction if it is not yet started. */ void trx_start_if_not_started_xa_low( @@ -2963,10 +3750,17 @@ void trx_start_if_not_started_xa_low( case TRX_STATE_ACTIVE: if (trx->id == 0 && read_write) { - /* If the transaction is tagged as read-only then - it can only write to temp tables and for such - transactions we don't want to move them to the - trx_sys_t::rw_trx_list. */ +#ifdef HAVE_LOCK_FREE_READVIEW + /* If the transaction is tagged as read-only then + it can only write to temp tables and for such + transactions we don't want to move them to the + trx_sys_t::rw_trx_hash. */ +#else + /* If the transaction is tagged as read-only then + it can only write to temp tables and for such + transactions we don't want to move them to the + trx_sys_t::rw_trx_list. */ +#endif /* HAVE_LOCK_FREE_READVIEW */ if (!trx->read_only) { trx_set_rw_mode(trx); } else if (!srv_read_only_mode) { @@ -3044,7 +3838,9 @@ void trx_start_internal_read_only_low(trx_t *trx) { void trx_set_rw_mode(trx_t *trx) /*!< in/out: transaction that is RW */ { ut_ad(trx->rsegs.m_redo.rseg == 0); +#ifndef HAVE_LOCK_FREE_READVIEW ut_ad(!trx->in_rw_trx_list); +#endif /* HAVE_LOCK_FREE_READVIEW */ ut_ad(!trx_is_autocommit_non_locking(trx)); ut_ad(!trx->read_only); @@ -3063,6 +3859,20 @@ void trx_set_rw_mode(trx_t *trx) /*!< in/out: transaction that is RW */ ut_ad(trx->rsegs.m_redo.rseg != 0); +#ifdef HAVE_LOCK_FREE_READVIEW + trx_sys->register_rw(trx); + + /* So that we can see our own changes. */ + if (trx->read_view.is_open()) { + trx->read_view.set_creator_trx_id(trx->id); + } + +#ifdef UNIV_DEBUG + if (trx->id > trx_sys->rw_max_trx_no) { + trx_sys->rw_max_trx_no = trx->id; + } +#endif /* UNIV_DEBUG */ +#else mutex_enter(&trx_sys->mutex); ut_ad(trx->id == 0); @@ -3082,6 +3892,7 @@ void trx_set_rw_mode(trx_t *trx) /*!< in/out: transaction that is RW */ ut_d(trx->in_rw_trx_list = true); mutex_exit(&trx_sys->mutex); +#endif /* HAVE_LOCK_FREE_READVIEW */ } void trx_kill_blocking(trx_t *trx) { diff --git a/storage/innobase/trx/trx0undo.cc b/storage/innobase/trx/trx0undo.cc index 7503d11..f963972 100644 --- a/storage/innobase/trx/trx0undo.cc +++ b/storage/innobase/trx/trx0undo.cc @@ -1885,6 +1885,71 @@ void trx_undo_insert_cleanup(trx_undo_ptr_t *undo_ptr, bool noredo) { mutex_exit(&(rseg->mutex)); } +#ifdef HAVE_LOCK_FREE_READVIEW +/** At shutdown, frees the undo logs of a transaction. */ +void trx_undo_free_at_shutdown(trx_t *trx) { + if (trx->rsegs.m_redo.update_undo != nullptr) { + switch (trx->rsegs.m_redo.update_undo->state) { + case TRX_UNDO_PREPARED: + break; + case TRX_UNDO_CACHED: + case TRX_UNDO_TO_FREE: + case TRX_UNDO_TO_PURGE: + case TRX_UNDO_ACTIVE: + ut_ad(trx_state_eq(trx, TRX_STATE_COMMITTED_IN_MEMORY)); + break; + default: + ut_error; + } + + UT_LIST_REMOVE(trx->rsegs.m_redo.rseg->update_undo_list, + trx->rsegs.m_redo.update_undo); + trx_undo_mem_free(trx->rsegs.m_redo.update_undo); + + trx->rsegs.m_redo.update_undo = nullptr; + } + + if (trx->rsegs.m_redo.insert_undo != nullptr) { + switch (trx->rsegs.m_redo.insert_undo->state) { + case TRX_UNDO_PREPARED: + break; + case TRX_UNDO_CACHED: + case TRX_UNDO_TO_FREE: + case TRX_UNDO_TO_PURGE: + case TRX_UNDO_ACTIVE: + ut_ad(trx_state_eq(trx, TRX_STATE_COMMITTED_IN_MEMORY)); + break; + default: + ut_error; + } + + UT_LIST_REMOVE(trx->rsegs.m_redo.rseg->insert_undo_list, + trx->rsegs.m_redo.insert_undo); + trx_undo_mem_free(trx->rsegs.m_redo.insert_undo); + + trx->rsegs.m_redo.insert_undo = nullptr; + } + + if (trx->rsegs.m_noredo.update_undo != nullptr) { + ut_a(trx->rsegs.m_noredo.update_undo->state == TRX_UNDO_PREPARED); + + UT_LIST_REMOVE(trx->rsegs.m_noredo.rseg->update_undo_list, + trx->rsegs.m_noredo.update_undo); + trx_undo_mem_free(trx->rsegs.m_noredo.update_undo); + + trx->rsegs.m_noredo.update_undo = nullptr; + } + if (trx->rsegs.m_noredo.insert_undo != nullptr) { + ut_a(trx->rsegs.m_noredo.insert_undo->state == TRX_UNDO_PREPARED); + + UT_LIST_REMOVE(trx->rsegs.m_noredo.rseg->insert_undo_list, + trx->rsegs.m_noredo.insert_undo); + trx_undo_mem_free(trx->rsegs.m_noredo.insert_undo); + + trx->rsegs.m_noredo.insert_undo = nullptr; + } +} +#else /** At shutdown, frees the undo logs of a PREPARED transaction. */ void trx_undo_free_prepared(trx_t *trx) /*!< in/out: PREPARED transaction */ { @@ -1927,6 +1992,7 @@ void trx_undo_free_prepared(trx_t *trx) /*!< in/out: PREPARED transaction */ trx->rsegs.m_noredo.insert_undo = NULL; } } +#endif /* HAVE_LOCK_FREE_READVIEW */ bool trx_undo_truncate_tablespace(undo::Tablespace *marked_space) { #ifdef UNIV_DEBUG diff --git a/storage/innobase/ut/ut0new.cc b/storage/innobase/ut/ut0new.cc index 46ce424..815803f 100644 --- a/storage/innobase/ut/ut0new.cc +++ b/storage/innobase/ut/ut0new.cc @@ -54,7 +54,9 @@ PSI_memory_key mem_key_partitioning; PSI_memory_key mem_key_row_log_buf; PSI_memory_key mem_key_row_merge_sort; PSI_memory_key mem_key_std; +#ifndef HAVE_LOCK_FREE_READVIEW PSI_memory_key mem_key_trx_sys_t_rw_trx_ids; +#endif /* HAVE_LOCK_FREE_READVIEW */ PSI_memory_key mem_key_undo_spaces; PSI_memory_key mem_key_ut_lock_free_hash_t; /* Please obey alphabetical order in the definitions above. */ @@ -94,8 +96,10 @@ static PSI_memory_info pfs_info[] = { {&mem_key_row_log_buf, "row_log_buf", 0, 0, PSI_DOCUMENT_ME}, {&mem_key_row_merge_sort, "row_merge_sort", 0, 0, PSI_DOCUMENT_ME}, {&mem_key_std, "std", 0, 0, PSI_DOCUMENT_ME}, +#ifndef HAVE_LOCK_FREE_READVIEW {&mem_key_trx_sys_t_rw_trx_ids, "trx_sys_t::rw_trx_ids", 0, 0, PSI_DOCUMENT_ME}, +#endif /* HAVE_LOCK_FREE_READVIEW */ {&mem_key_undo_spaces, "undo::Tablespaces", 0, 0, PSI_DOCUMENT_ME}, {&mem_key_ut_lock_free_hash_t, "ut_lock_free_hash_t", 0, 0, PSI_DOCUMENT_ME}, diff --git a/unittest/gunit/mysys_lf-t.cc b/unittest/gunit/mysys_lf-t.cc index d658834..7a5a30d 100644 --- a/unittest/gunit/mysys_lf-t.cc +++ b/unittest/gunit/mysys_lf-t.cc @@ -122,10 +122,20 @@ extern "C" void *test_lf_alloc(void *arg) { return 0; } +#ifdef HAVE_LOCK_FREE_READVIEW +bool do_sum(void *num, void *acc) { + *static_cast(acc) += *static_cast(num); + return false; +} +#endif /* HAVE_LOCK_FREE_READVIEW */ const int N_TLH = 1000; extern "C" void *test_lf_hash(void *arg) { int m = (*(int *)arg) / (2 * N_TLH); +#ifdef HAVE_LOCK_FREE_READVIEW + int32 x, y, z, sum = 0, ins = 0, scans = 0; +#else int32 x, y, z, sum = 0, ins = 0; +#endif /* HAVE_LOCK_FREE_READVIEW */ LF_PINS *pins; if (with_my_thread_init) my_thread_init(); @@ -142,6 +152,13 @@ extern "C" void *test_lf_hash(void *arg) { sum += z; ins++; } +#ifdef HAVE_LOCK_FREE_READVIEW + else { + int unused = 0; + lf_hash_iterate(&lf_hash, pins, do_sum, &unused); + scans++; + } +#endif /* HAVE_LOCK_FREE_READVIEW */ } for (i = 0; i < N_TLH; i++) { y = (y * (m + i) + 0x87654321) & INT_MAX32; -- 1.8.3.1