diff --git a/mysql-test/suite/innodb/r/clone_rc_reopen_stale_view_crash.result b/mysql-test/suite/innodb/r/clone_rc_reopen_stale_view_crash.result new file mode 100644 index 000000000000..c2a6c4c2f914 --- /dev/null +++ b/mysql-test/suite/innodb/r/clone_rc_reopen_stale_view_crash.result @@ -0,0 +1,22 @@ +CREATE TABLE t1(id INT PRIMARY KEY, c INT, pad VARCHAR(200)) ENGINE=InnoDB; +INSERT INTO t1 VALUES(1, 0, REPEAT('a', 200)); +SET SESSION transaction_isolation='READ-COMMITTED'; +SELECT c FROM t1 WHERE id=1; +c +0 +SET GLOBAL innodb_purge_run_now=ON; +SET DEBUG_SYNC='mvcc_view_open_after_untag_before_reopen SIGNAL donor_before_reopen WAIT_FOR continue_donor TIMEOUT 120'; +SELECT c FROM t1 WHERE id=1; +SET SESSION transaction_isolation='REPEATABLE-READ'; +SET DEBUG_SYNC='now WAIT_FOR donor_before_reopen TIMEOUT 120'; +Warnings: +Warning 138 InnoDB: WITH CONSISTENT SNAPSHOT FROM SESSION was ignored because the target transaction has not been assigned a read view. +SELECT c FROM t1 WHERE id=1; +c +200 +SET DEBUG_SYNC='now SIGNAL continue_donor'; +c +200 +ROLLBACK; +DROP TABLE IF EXISTS t1; +SET DEBUG_SYNC='RESET'; diff --git a/mysql-test/suite/innodb/t/clone_rc_reopen_stale_view_crash.test b/mysql-test/suite/innodb/t/clone_rc_reopen_stale_view_crash.test new file mode 100644 index 000000000000..b285b929d556 --- /dev/null +++ b/mysql-test/suite/innodb/t/clone_rc_reopen_stale_view_crash.test @@ -0,0 +1,87 @@ +# Regression coverage for rejecting a donor RC reusable read view while the +# donor is reopening it for the next consistent read. The donor's previous +# statement view is a closed reusable view and does not protect undo from purge. +# After purge removes the needed undo records, cloning that stale view must be +# ignored instead of letting the receiver read through purged undo. + +--source include/have_debug.inc +--source include/have_debug_sync.inc +--source include/not_valgrind.inc +--source include/not_crashrep.inc +--source include/count_sessions.inc + +--disable_query_log +CALL mtr.add_suppression("Required history data has been deleted"); +--enable_query_log + +CREATE TABLE t1(id INT PRIMARY KEY, c INT, pad VARCHAR(200)) ENGINE=InnoDB; +INSERT INTO t1 VALUES(1, 0, REPEAT('a', 200)); + +--connect (receiver,localhost,root,,) + +--connection default +--let $donor_id=`SELECT CONNECTION_ID()` +SET SESSION transaction_isolation='READ-COMMITTED'; + +# Open an RC read view and close it at statement end as a reusable view. +SELECT c FROM t1 WHERE id=1; + +# Make the reusable view stale and generate undo that the stale view would need +# in order to reconstruct the old version. +--connection receiver +--disable_query_log +--let $i= 1 +while ($i <= 200) +{ + --eval UPDATE t1 SET c = $i, pad = REPEAT(CHAR(65 + ($i % 26)), 200) WHERE id = 1 + --inc $i +} +--enable_query_log + +# There is no active read view protecting the donor's previous statement view: +# it is a closed reusable view. Force purge before cloning it back into the +# receiver transaction. +SET GLOBAL innodb_purge_run_now=ON; +--let $wait_timeout= 60 +--source include/wait_innodb_all_purged.inc +--source suite/innodb/include/force_purge.inc + +--connection default +# Start the donor's next RC autocommit consistent read and stop it exactly after +# MVCC::view_open() has cleared the reusable-view pointer tag, but before the +# view is reopened and linked back into the active MVCC view list. At this point +# trx->read_view looks like a normal pointer to trx_clone_read_view(), while the +# underlying ReadView is still the old closed view that purge did not protect. +SET DEBUG_SYNC='mvcc_view_open_after_untag_before_reopen SIGNAL donor_before_reopen WAIT_FOR continue_donor TIMEOUT 120'; +--send SELECT c FROM t1 WHERE id=1 + +--connection receiver +SET SESSION transaction_isolation='REPEATABLE-READ'; +SET DEBUG_SYNC='now WAIT_FOR donor_before_reopen TIMEOUT 120'; + +# This used to be the buggy window: trx_clone_read_view() only checked that the +# donor transaction was ACTIVE and had a non-NULL read_view, so it cloned this +# untagged-but-still-closed reusable view. The fixed behavior is to reject the +# stale donor snapshot and emit a warning that the cloned snapshot was ignored. +--disable_query_log +--eval START TRANSACTION WITH CONSISTENT SNAPSHOT FROM SESSION $donor_id +--enable_query_log + +# Since the stale donor view was rejected, the receiver gets a normal fresh read +# view for its first consistent read and sees the latest committed version. +SELECT c FROM t1 WHERE id=1; + +SET DEBUG_SYNC='now SIGNAL continue_donor'; + +--connection default +--reap + +--connection receiver +ROLLBACK; + +--connection default +DROP TABLE IF EXISTS t1; +SET DEBUG_SYNC='RESET'; + +--disconnect receiver +--source include/wait_until_count_sessions.inc diff --git a/storage/innobase/read/read0read.cc b/storage/innobase/read/read0read.cc index ad2ee4e0a3ce..ea4289d55e20 100644 --- a/storage/innobase/read/read0read.cc +++ b/storage/innobase/read/read0read.cc @@ -553,6 +553,8 @@ void MVCC::view_open(ReadView *&view, trx_t *trx) { view = reinterpret_cast(p & ~1); + DEBUG_SYNC_C("mvcc_view_open_after_untag_before_reopen"); + ut_ad(view->m_closed); /* NOTE: This can be optimised further, for now we only diff --git a/storage/innobase/trx/trx0trx.cc b/storage/innobase/trx/trx0trx.cc index 264e5ee9185f..8ef104a5f230 100644 --- a/storage/innobase/trx/trx0trx.cc +++ b/storage/innobase/trx/trx0trx.cc @@ -2349,6 +2349,7 @@ ReadView *trx_assign_read_view(trx_t *trx) /*!< in/out: active transaction */ return (nullptr); } else if (!MVCC::is_view_active(trx->read_view)) { + DEBUG_SYNC_C("trx_assign_read_view_before_view_open"); trx_sys->mvcc->view_open(trx->read_view, trx); } @@ -2372,7 +2373,8 @@ ReadView *trx_clone_read_view(trx_t *trx, trx_t *from_trx) { return (nullptr); } - if (from_trx->state != TRX_STATE_ACTIVE || from_trx->read_view == nullptr) { + if (from_trx->state != TRX_STATE_ACTIVE || from_trx->read_view == nullptr || + !MVCC::is_view_active(from_trx->read_view)) { trx_sys_mutex_exit(); trx_mutex_exit(from_trx); return (nullptr); @@ -2382,6 +2384,23 @@ ReadView *trx_clone_read_view(trx_t *trx, trx_t *from_trx) { from_trx->read_view->clone(trx->read_view, from_trx); + if (from_trx->isolation_level == TRX_ISO_READ_COMMITTED && + trx->read_view->low_limit_no() <= purge_sys->view.low_limit_no()) { +#ifdef UNIV_DEBUG + ib::info() << "[read_view_reuse] cloned RC read view rejected: empty=" + << trx->read_view->empty() + << ", trx low_limit_no=" << trx->read_view->low_limit_no() + << ", purge low_limit_no=" << purge_sys->view.low_limit_no(); +#endif + /* clone() may allocate a fresh view from MVCC::m_free. Put it on + m_views before closing so view_close() can consistently recycle it and + clear trx->read_view. */ + if (needs_adding) trx_sys->mvcc->view_add(trx->read_view); + trx_sys->mvcc->view_close(trx->read_view, true); + trx_sys_mutex_exit(); + trx_mutex_exit(from_trx); + return (nullptr); + } trx_mutex_exit(from_trx); if (needs_adding) trx_sys->mvcc->view_add(trx->read_view);