Skip to content

Commit 18eab4a

Browse files
committed
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
1 parent 9068020 commit 18eab4a

File tree

6 files changed

+279
-2
lines changed

6 files changed

+279
-2
lines changed

mysql-test/suite/rpl/r/rpl_xa.result

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,4 +219,65 @@ include/sync_with_master_gtid.inc
219219
connection master;
220220
drop database test_ign;
221221
drop table t1, t2, t3, tm;
222+
#
223+
# MDEV-26682 slave lock timeout with XA and gap locks
224+
#
225+
create table t1 (a int primary key, b int unique) engine=innodb;
226+
insert t1 values (1,1),(3,3),(5,5);
227+
connection slave;
228+
set session tx_isolation='repeatable-read';
229+
start transaction;
230+
select * from t1;
231+
a b
232+
1 1
233+
3 3
234+
5 5
235+
connect m2, localhost, root;
236+
delete from t1 where a=3;
237+
xa start 'x1';
238+
update t1 set b=3 where a=5;
239+
xa end 'x1';
240+
xa prepare 'x1';
241+
connect m3, localhost, root;
242+
insert t1 values (2, 2);
243+
-->slave
244+
connection slave;
245+
commit;
246+
select * from t1;
247+
a b
248+
1 1
249+
2 2
250+
5 5
251+
connection m2;
252+
xa rollback 'x1';
253+
disconnect m2;
254+
disconnect m3;
255+
connection master;
256+
drop table t1;
257+
create table t1 (id int auto_increment primary key, c1 int not null unique)
258+
engine=innodb;
259+
create table t2 (id int auto_increment primary key, c1 int not null,
260+
foreign key(c1) references t1(c1), unique key(c1)) engine=innodb;
261+
insert t1 values (869,1), (871,3), (873,4), (872,5), (870,6), (877,7);
262+
insert t2 values (795,6), (800,7);
263+
xa start '1';
264+
update t2 set id = 9, c1 = 5 where c1 in (null, null, null, null, null, 7, 3);
265+
connect con1, localhost,root;
266+
xa start '2';
267+
delete from t1 where c1 like '3%';
268+
xa end '2';
269+
xa prepare '2';
270+
connection master;
271+
xa end '1';
272+
xa prepare '1';
273+
->slave
274+
connection slave;
275+
connection slave;
276+
include/sync_with_master_gtid.inc
277+
connection con1;
278+
xa commit '2';
279+
disconnect con1;
280+
connection master;
281+
xa commit '1';
282+
drop table t2, t1;
222283
include/rpl_end.inc

mysql-test/suite/rpl/r/rpl_xa_gtid_pos_auto_engine.result

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,67 @@ include/sync_with_master_gtid.inc
228228
connection master;
229229
drop database test_ign;
230230
drop table t1, t2, t3, tm;
231+
#
232+
# MDEV-26682 slave lock timeout with XA and gap locks
233+
#
234+
create table t1 (a int primary key, b int unique) engine=innodb;
235+
insert t1 values (1,1),(3,3),(5,5);
236+
connection slave;
237+
set session tx_isolation='repeatable-read';
238+
start transaction;
239+
select * from t1;
240+
a b
241+
1 1
242+
3 3
243+
5 5
244+
connect m2, localhost, root;
245+
delete from t1 where a=3;
246+
xa start 'x1';
247+
update t1 set b=3 where a=5;
248+
xa end 'x1';
249+
xa prepare 'x1';
250+
connect m3, localhost, root;
251+
insert t1 values (2, 2);
252+
-->slave
253+
connection slave;
254+
commit;
255+
select * from t1;
256+
a b
257+
1 1
258+
2 2
259+
5 5
260+
connection m2;
261+
xa rollback 'x1';
262+
disconnect m2;
263+
disconnect m3;
264+
connection master;
265+
drop table t1;
266+
create table t1 (id int auto_increment primary key, c1 int not null unique)
267+
engine=innodb;
268+
create table t2 (id int auto_increment primary key, c1 int not null,
269+
foreign key(c1) references t1(c1), unique key(c1)) engine=innodb;
270+
insert t1 values (869,1), (871,3), (873,4), (872,5), (870,6), (877,7);
271+
insert t2 values (795,6), (800,7);
272+
xa start '1';
273+
update t2 set id = 9, c1 = 5 where c1 in (null, null, null, null, null, 7, 3);
274+
connect con1, localhost,root;
275+
xa start '2';
276+
delete from t1 where c1 like '3%';
277+
xa end '2';
278+
xa prepare '2';
279+
connection master;
280+
xa end '1';
281+
xa prepare '1';
282+
->slave
283+
connection slave;
284+
connection slave;
285+
include/sync_with_master_gtid.inc
286+
connection con1;
287+
xa commit '2';
288+
disconnect con1;
289+
connection master;
290+
xa commit '1';
291+
drop table t2, t1;
231292
connection slave;
232293
include/stop_slave.inc
233294
SET @@global.gtid_pos_auto_engines="";

mysql-test/suite/rpl/t/rpl_xa.inc

Lines changed: 79 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
#
22
# This "body" file checks general properties of XA transaction replication
3-
# as of MDEV-7974.
3+
# as of MDEV-742.
44
# Parameters:
55
# --let rpl_xa_check= SELECT ...
66
#
@@ -353,3 +353,81 @@ source include/sync_with_master_gtid.inc;
353353
connection master;
354354
--eval drop database test_ign
355355
drop table t1, t2, t3, tm;
356+
357+
--echo #
358+
--echo # MDEV-26682 slave lock timeout with XA and gap locks
359+
--echo #
360+
create table t1 (a int primary key, b int unique) engine=innodb;
361+
insert t1 values (1,1),(3,3),(5,5);
362+
sync_slave_with_master;
363+
364+
# set a strong isolation level to keep the read view below.
365+
# alternatively a long-running select can do that too even in read-committed
366+
set session tx_isolation='repeatable-read';
367+
start transaction;
368+
# opens a read view to disable purge on the slave
369+
select * from t1;
370+
371+
connect m2, localhost, root;
372+
# now, delete a value, purge it on the master, but not on the slave
373+
delete from t1 where a=3;
374+
xa start 'x1';
375+
# this sets a gap lock on <3>, when it exists (so, on the slave)
376+
update t1 set b=3 where a=5;
377+
xa end 'x1';
378+
xa prepare 'x1';
379+
380+
connect m3, localhost, root;
381+
# and this tries to insert straight into the locked gap
382+
insert t1 values (2, 2);
383+
384+
echo -->slave;
385+
sync_slave_with_master;
386+
commit;
387+
select * from t1;
388+
389+
connection m2;
390+
xa rollback 'x1';
391+
disconnect m2;
392+
disconnect m3;
393+
394+
connection master;
395+
396+
drop table t1;
397+
398+
create table t1 (id int auto_increment primary key, c1 int not null unique)
399+
engine=innodb;
400+
401+
create table t2 (id int auto_increment primary key, c1 int not null,
402+
foreign key(c1) references t1(c1), unique key(c1)) engine=innodb;
403+
404+
insert t1 values (869,1), (871,3), (873,4), (872,5), (870,6), (877,7);
405+
insert t2 values (795,6), (800,7);
406+
407+
xa start '1';
408+
update t2 set id = 9, c1 = 5 where c1 in (null, null, null, null, null, 7, 3);
409+
410+
connect con1, localhost,root;
411+
xa start '2';
412+
delete from t1 where c1 like '3%';
413+
xa end '2';
414+
xa prepare '2';
415+
416+
connection master;
417+
xa end '1';
418+
xa prepare '1';
419+
420+
echo ->slave;
421+
422+
sync_slave_with_master;
423+
424+
connection slave;
425+
source include/sync_with_master_gtid.inc;
426+
427+
connection con1;
428+
xa commit '2';
429+
disconnect con1;
430+
431+
connection master;
432+
xa commit '1';
433+
drop table t2, t1;

storage/innobase/include/lock0lock.h

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/*****************************************************************************
22
33
Copyright (c) 1996, 2016, Oracle and/or its affiliates. All Rights Reserved.
4-
Copyright (c) 2017, 2020, MariaDB Corporation.
4+
Copyright (c) 2017, 2021, MariaDB Corporation.
55
66
This program is free software; you can redistribute it and/or modify it under
77
the terms of the GNU General Public License as published by the Free Software
@@ -478,6 +478,10 @@ lock_rec_unlock(
478478
and release possible other transactions waiting because of these locks. */
479479
void lock_release(trx_t* trx);
480480

481+
/** Release non-exclusive locks on XA PREPARE,
482+
and release possible other transactions waiting because of these locks. */
483+
void lock_release_on_prepare(trx_t *trx);
484+
481485
/*************************************************************//**
482486
Get the lock hash table */
483487
UNIV_INLINE

storage/innobase/lock/lock0lock.cc

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4219,6 +4219,65 @@ void lock_release(trx_t* trx)
42194219
#endif
42204220
}
42214221

4222+
/** Release non-exclusive locks on XA PREPARE,
4223+
and release possible other transactions waiting because of these locks. */
4224+
void lock_release_on_prepare(trx_t *trx)
4225+
{
4226+
ulint count= 0;
4227+
lock_mutex_enter();
4228+
ut_ad(!trx_mutex_own(trx));
4229+
4230+
for (lock_t *lock= UT_LIST_GET_LAST(trx->lock.trx_locks); lock; )
4231+
{
4232+
ut_ad(lock->trx == trx);
4233+
4234+
if (lock_get_type_low(lock) == LOCK_REC)
4235+
{
4236+
ut_ad(!lock->index->table->is_temporary());
4237+
if (lock_rec_get_gap(lock) || lock_get_mode(lock) != LOCK_X)
4238+
lock_rec_dequeue_from_page(lock);
4239+
else
4240+
{
4241+
ut_ad(trx->dict_operation ||
4242+
lock->index->table->id >= DICT_HDR_FIRST_ID);
4243+
retain_lock:
4244+
lock= UT_LIST_GET_PREV(trx_locks, lock);
4245+
continue;
4246+
}
4247+
}
4248+
else
4249+
{
4250+
ut_ad(lock_get_type_low(lock) & LOCK_TABLE);
4251+
dict_table_t *table= lock->un_member.tab_lock.table;
4252+
ut_ad(!table->is_temporary());
4253+
4254+
switch (lock_get_mode(lock)) {
4255+
case LOCK_IS:
4256+
case LOCK_S:
4257+
lock_table_dequeue(lock);
4258+
break;
4259+
case LOCK_IX:
4260+
case LOCK_X:
4261+
ut_ad(table->id >= DICT_HDR_FIRST_ID || trx->dict_operation);
4262+
/* fall through */
4263+
default:
4264+
goto retain_lock;
4265+
}
4266+
}
4267+
4268+
if (++count == LOCK_RELEASE_INTERVAL)
4269+
{
4270+
lock_mutex_exit();
4271+
count= 0;
4272+
lock_mutex_enter();
4273+
}
4274+
4275+
lock= UT_LIST_GET_LAST(trx->lock.trx_locks);
4276+
}
4277+
4278+
lock_mutex_exit();
4279+
}
4280+
42224281
/* True if a lock mode is S or X */
42234282
#define IS_LOCK_S_OR_X(lock) \
42244283
(lock_get_mode(lock) == LOCK_S \

storage/innobase/trx/trx0trx.cc

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1971,6 +1971,20 @@ trx_prepare(
19711971
We must not be holding any mutexes or latches here. */
19721972

19731973
trx_flush_log_if_needed(lsn, trx);
1974+
1975+
if (!UT_LIST_GET_LEN(trx->lock.trx_locks)
1976+
|| trx->isolation_level == TRX_ISO_SERIALIZABLE) {
1977+
/* Do not release any locks at the
1978+
SERIALIZABLE isolation level. */
1979+
} else if (!trx->mysql_thd
1980+
|| thd_sql_command(trx->mysql_thd)
1981+
!= SQLCOM_XA_PREPARE) {
1982+
/* Do not release locks for XA COMMIT ONE PHASE
1983+
or for internal distributed transactions
1984+
(XID::get_my_xid() would be nonzero). */
1985+
} else {
1986+
lock_release_on_prepare(trx);
1987+
}
19741988
}
19751989
}
19761990

0 commit comments

Comments
 (0)