diff options
author | Marko Mäkelä <marko.makela@mariadb.com> | 2021-10-18 12:49:10 +0300 |
---|---|---|
committer | Marko Mäkelä <marko.makela@mariadb.com> | 2021-10-18 12:49:10 +0300 |
commit | 18eab4a83280049974265358b0d78389d05cd67b (patch) | |
tree | 2398474f2621a04cfacad05225275701fcc0c922 | |
parent | 9068020efe8f8b0161ea0e48a89fb90fcd8e59aa (diff) | |
download | mariadb-git-18eab4a83280049974265358b0d78389d05cd67b.tar.gz |
MDEV-26682 Replication timeouts with XA PREPAREbb-10.5-MDEV-26682
The purpose of non-exclusive locks in a transaction is to guarantee
that the records covered by those locks must remain in that way until
the transaction is committed. (The purpose of gap locks is to ensure
that a record that was nonexistent will remain that way.)
Once a transaction has reached the XA PREPARE state, the only allowed
further actions are XA ROLLBACK or XA COMMIT. Therefore, it can be
argued that only the exclusive locks that the XA PREPARE transaction
is holding are essential.
Furthermore, InnoDB never preserved explicit locks across server restart.
For XA PREPARE transations, we will only recover implicit exclusive locks
for records that had been modified.
Because of the fact that XA PREPARE followed by a server restart will
cause some locks to be lost, we might as well always release all
non-exclusive locks during the execution of an XA PREPARE statement.
lock_release_on_prepare(): Release non-exclusive locks on XA PREPARE.
trx_prepare(): Invoke lock_release_on_prepare() unless the
isolation level is SERIALIZABLE or this is an internal distributed
transaction with the binlog (not actual XA PREPARE statement).
This has been discussed with Sergei Golubchik and Andrei Elkin.
Reviewed by: Sergei Golubchik
-rw-r--r-- | mysql-test/suite/rpl/r/rpl_xa.result | 61 | ||||
-rw-r--r-- | mysql-test/suite/rpl/r/rpl_xa_gtid_pos_auto_engine.result | 61 | ||||
-rw-r--r-- | mysql-test/suite/rpl/t/rpl_xa.inc | 80 | ||||
-rw-r--r-- | storage/innobase/include/lock0lock.h | 6 | ||||
-rw-r--r-- | storage/innobase/lock/lock0lock.cc | 59 | ||||
-rw-r--r-- | storage/innobase/trx/trx0trx.cc | 14 |
6 files changed, 279 insertions, 2 deletions
diff --git a/mysql-test/suite/rpl/r/rpl_xa.result b/mysql-test/suite/rpl/r/rpl_xa.result index a90e6e0b996..061c7b360d0 100644 --- a/mysql-test/suite/rpl/r/rpl_xa.result +++ b/mysql-test/suite/rpl/r/rpl_xa.result @@ -219,4 +219,65 @@ include/sync_with_master_gtid.inc connection master; drop database test_ign; drop table t1, t2, t3, tm; +# +# MDEV-26682 slave lock timeout with XA and gap locks +# +create table t1 (a int primary key, b int unique) engine=innodb; +insert t1 values (1,1),(3,3),(5,5); +connection slave; +set session tx_isolation='repeatable-read'; +start transaction; +select * from t1; +a b +1 1 +3 3 +5 5 +connect m2, localhost, root; +delete from t1 where a=3; +xa start 'x1'; +update t1 set b=3 where a=5; +xa end 'x1'; +xa prepare 'x1'; +connect m3, localhost, root; +insert t1 values (2, 2); +-->slave +connection slave; +commit; +select * from t1; +a b +1 1 +2 2 +5 5 +connection m2; +xa rollback 'x1'; +disconnect m2; +disconnect m3; +connection master; +drop table t1; +create table t1 (id int auto_increment primary key, c1 int not null unique) +engine=innodb; +create table t2 (id int auto_increment primary key, c1 int not null, +foreign key(c1) references t1(c1), unique key(c1)) engine=innodb; +insert t1 values (869,1), (871,3), (873,4), (872,5), (870,6), (877,7); +insert t2 values (795,6), (800,7); +xa start '1'; +update t2 set id = 9, c1 = 5 where c1 in (null, null, null, null, null, 7, 3); +connect con1, localhost,root; +xa start '2'; +delete from t1 where c1 like '3%'; +xa end '2'; +xa prepare '2'; +connection master; +xa end '1'; +xa prepare '1'; +->slave +connection slave; +connection slave; +include/sync_with_master_gtid.inc +connection con1; +xa commit '2'; +disconnect con1; +connection master; +xa commit '1'; +drop table t2, t1; include/rpl_end.inc diff --git a/mysql-test/suite/rpl/r/rpl_xa_gtid_pos_auto_engine.result b/mysql-test/suite/rpl/r/rpl_xa_gtid_pos_auto_engine.result index ffd0426ab0d..35625cc7026 100644 --- a/mysql-test/suite/rpl/r/rpl_xa_gtid_pos_auto_engine.result +++ b/mysql-test/suite/rpl/r/rpl_xa_gtid_pos_auto_engine.result @@ -228,6 +228,67 @@ include/sync_with_master_gtid.inc connection master; drop database test_ign; drop table t1, t2, t3, tm; +# +# MDEV-26682 slave lock timeout with XA and gap locks +# +create table t1 (a int primary key, b int unique) engine=innodb; +insert t1 values (1,1),(3,3),(5,5); +connection slave; +set session tx_isolation='repeatable-read'; +start transaction; +select * from t1; +a b +1 1 +3 3 +5 5 +connect m2, localhost, root; +delete from t1 where a=3; +xa start 'x1'; +update t1 set b=3 where a=5; +xa end 'x1'; +xa prepare 'x1'; +connect m3, localhost, root; +insert t1 values (2, 2); +-->slave +connection slave; +commit; +select * from t1; +a b +1 1 +2 2 +5 5 +connection m2; +xa rollback 'x1'; +disconnect m2; +disconnect m3; +connection master; +drop table t1; +create table t1 (id int auto_increment primary key, c1 int not null unique) +engine=innodb; +create table t2 (id int auto_increment primary key, c1 int not null, +foreign key(c1) references t1(c1), unique key(c1)) engine=innodb; +insert t1 values (869,1), (871,3), (873,4), (872,5), (870,6), (877,7); +insert t2 values (795,6), (800,7); +xa start '1'; +update t2 set id = 9, c1 = 5 where c1 in (null, null, null, null, null, 7, 3); +connect con1, localhost,root; +xa start '2'; +delete from t1 where c1 like '3%'; +xa end '2'; +xa prepare '2'; +connection master; +xa end '1'; +xa prepare '1'; +->slave +connection slave; +connection slave; +include/sync_with_master_gtid.inc +connection con1; +xa commit '2'; +disconnect con1; +connection master; +xa commit '1'; +drop table t2, t1; connection slave; include/stop_slave.inc SET @@global.gtid_pos_auto_engines=""; diff --git a/mysql-test/suite/rpl/t/rpl_xa.inc b/mysql-test/suite/rpl/t/rpl_xa.inc index 38344da5e66..d22d2d2ef3d 100644 --- a/mysql-test/suite/rpl/t/rpl_xa.inc +++ b/mysql-test/suite/rpl/t/rpl_xa.inc @@ -1,6 +1,6 @@ # # This "body" file checks general properties of XA transaction replication -# as of MDEV-7974. +# as of MDEV-742. # Parameters: # --let rpl_xa_check= SELECT ... # @@ -353,3 +353,81 @@ source include/sync_with_master_gtid.inc; connection master; --eval drop database test_ign drop table t1, t2, t3, tm; + +--echo # +--echo # MDEV-26682 slave lock timeout with XA and gap locks +--echo # +create table t1 (a int primary key, b int unique) engine=innodb; +insert t1 values (1,1),(3,3),(5,5); +sync_slave_with_master; + +# set a strong isolation level to keep the read view below. +# alternatively a long-running select can do that too even in read-committed +set session tx_isolation='repeatable-read'; +start transaction; +# opens a read view to disable purge on the slave +select * from t1; + +connect m2, localhost, root; +# now, delete a value, purge it on the master, but not on the slave +delete from t1 where a=3; +xa start 'x1'; +# this sets a gap lock on <3>, when it exists (so, on the slave) +update t1 set b=3 where a=5; +xa end 'x1'; +xa prepare 'x1'; + +connect m3, localhost, root; +# and this tries to insert straight into the locked gap +insert t1 values (2, 2); + +echo -->slave; +sync_slave_with_master; +commit; +select * from t1; + +connection m2; +xa rollback 'x1'; +disconnect m2; +disconnect m3; + +connection master; + +drop table t1; + +create table t1 (id int auto_increment primary key, c1 int not null unique) +engine=innodb; + +create table t2 (id int auto_increment primary key, c1 int not null, +foreign key(c1) references t1(c1), unique key(c1)) engine=innodb; + +insert t1 values (869,1), (871,3), (873,4), (872,5), (870,6), (877,7); +insert t2 values (795,6), (800,7); + +xa start '1'; +update t2 set id = 9, c1 = 5 where c1 in (null, null, null, null, null, 7, 3); + +connect con1, localhost,root; +xa start '2'; +delete from t1 where c1 like '3%'; +xa end '2'; +xa prepare '2'; + +connection master; +xa end '1'; +xa prepare '1'; + +echo ->slave; + +sync_slave_with_master; + +connection slave; +source include/sync_with_master_gtid.inc; + +connection con1; +xa commit '2'; +disconnect con1; + +connection master; +xa commit '1'; +drop table t2, t1; diff --git a/storage/innobase/include/lock0lock.h b/storage/innobase/include/lock0lock.h index 3b63b06a9bb..225c246f4e7 100644 --- a/storage/innobase/include/lock0lock.h +++ b/storage/innobase/include/lock0lock.h @@ -1,7 +1,7 @@ /***************************************************************************** Copyright (c) 1996, 2016, Oracle and/or its affiliates. All Rights Reserved. -Copyright (c) 2017, 2020, MariaDB Corporation. +Copyright (c) 2017, 2021, MariaDB Corporation. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software @@ -478,6 +478,10 @@ lock_rec_unlock( and release possible other transactions waiting because of these locks. */ void lock_release(trx_t* trx); +/** Release non-exclusive locks on XA PREPARE, +and release possible other transactions waiting because of these locks. */ +void lock_release_on_prepare(trx_t *trx); + /*************************************************************//** Get the lock hash table */ UNIV_INLINE diff --git a/storage/innobase/lock/lock0lock.cc b/storage/innobase/lock/lock0lock.cc index d7ec5736826..12764470bef 100644 --- a/storage/innobase/lock/lock0lock.cc +++ b/storage/innobase/lock/lock0lock.cc @@ -4219,6 +4219,65 @@ void lock_release(trx_t* trx) #endif } +/** Release non-exclusive locks on XA PREPARE, +and release possible other transactions waiting because of these locks. */ +void lock_release_on_prepare(trx_t *trx) +{ + ulint count= 0; + lock_mutex_enter(); + ut_ad(!trx_mutex_own(trx)); + + for (lock_t *lock= UT_LIST_GET_LAST(trx->lock.trx_locks); lock; ) + { + ut_ad(lock->trx == trx); + + if (lock_get_type_low(lock) == LOCK_REC) + { + ut_ad(!lock->index->table->is_temporary()); + if (lock_rec_get_gap(lock) || lock_get_mode(lock) != LOCK_X) + lock_rec_dequeue_from_page(lock); + else + { + ut_ad(trx->dict_operation || + lock->index->table->id >= DICT_HDR_FIRST_ID); +retain_lock: + lock= UT_LIST_GET_PREV(trx_locks, lock); + continue; + } + } + else + { + ut_ad(lock_get_type_low(lock) & LOCK_TABLE); + dict_table_t *table= lock->un_member.tab_lock.table; + ut_ad(!table->is_temporary()); + + switch (lock_get_mode(lock)) { + case LOCK_IS: + case LOCK_S: + lock_table_dequeue(lock); + break; + case LOCK_IX: + case LOCK_X: + ut_ad(table->id >= DICT_HDR_FIRST_ID || trx->dict_operation); + /* fall through */ + default: + goto retain_lock; + } + } + + if (++count == LOCK_RELEASE_INTERVAL) + { + lock_mutex_exit(); + count= 0; + lock_mutex_enter(); + } + + lock= UT_LIST_GET_LAST(trx->lock.trx_locks); + } + + lock_mutex_exit(); +} + /* True if a lock mode is S or X */ #define IS_LOCK_S_OR_X(lock) \ (lock_get_mode(lock) == LOCK_S \ diff --git a/storage/innobase/trx/trx0trx.cc b/storage/innobase/trx/trx0trx.cc index cf8fa17cf1a..16ea7c41d71 100644 --- a/storage/innobase/trx/trx0trx.cc +++ b/storage/innobase/trx/trx0trx.cc @@ -1971,6 +1971,20 @@ trx_prepare( We must not be holding any mutexes or latches here. */ trx_flush_log_if_needed(lsn, trx); + + if (!UT_LIST_GET_LEN(trx->lock.trx_locks) + || trx->isolation_level == TRX_ISO_SERIALIZABLE) { + /* Do not release any locks at the + SERIALIZABLE isolation level. */ + } else if (!trx->mysql_thd + || thd_sql_command(trx->mysql_thd) + != SQLCOM_XA_PREPARE) { + /* Do not release locks for XA COMMIT ONE PHASE + or for internal distributed transactions + (XID::get_my_xid() would be nonzero). */ + } else { + lock_release_on_prepare(trx); + } } } |