Commit 18eab4a8 authored by Marko Mäkelä's avatar Marko Mäkelä

MDEV-26682 Replication timeouts with XA PREPARE

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
parent 9068020e
......@@ -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
......@@ -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="";
......
#
# 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;
/*****************************************************************************
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
......
......@@ -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 \
......
......@@ -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);
}
}
}
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment