Skip to content

Commit c3c5392

Browse files
committed
MDEV-26554: Races between INSERT on child and DDL on parent table
The SQL layer never acquires metadata locks (MDL) on the tables that the tables that DML statement accesses is modifying. However, the storage engine must access the parent table in order to ensure that the child table will not refer to a non-existing record in the parent table. During certain DDL operations, the InnoDB table metadata (dict_table_t) may be be freed and rebuilt. This would cause a race condition with a concurrent INSERT that is attempting to report a FOREIGN KEY violation. We work around the insufficient MDL during DML by acquiring exclusive InnoDB table locks on all child tables during DDL. To avoid deadlocks, we will follow the following order of acquisition: 1. tables whose REFERENCES clauses point to the current table 2. the current table that is being subjected to DDL 3. mysql.innodb_table_stats 4. mysql.innodb_index_stats 5. the InnoDB dictionary tables (SYS_TABLES and so on) 6. exclusive dict_sys.latch
1 parent 59fe6a8 commit c3c5392

File tree

7 files changed

+130
-18
lines changed

7 files changed

+130
-18
lines changed

mysql-test/suite/innodb/r/foreign_key.result

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -408,8 +408,8 @@ INSERT INTO t1 VALUES (1,2);
408408
CREATE TABLE x AS SELECT * FROM t1;
409409
ERROR XAE07: XAER_RMFAIL: The command cannot be executed when global transaction is in the ACTIVE state
410410
connect con1,localhost,root,,test;
411-
SET foreign_key_checks= OFF, innodb_lock_wait_timeout= 1;
412-
SET lock_wait_timeout=5;
411+
SET foreign_key_checks= OFF, innodb_lock_wait_timeout= 0;
412+
SET lock_wait_timeout=2;
413413
ALTER TABLE t1 ADD FOREIGN KEY f (a) REFERENCES t1 (pk), LOCK=EXCLUSIVE;
414414
ERROR HY000: Lock wait timeout exceeded; try restarting transaction
415415
disconnect con1;
@@ -491,7 +491,7 @@ BEGIN;
491491
UPDATE users SET name = 'qux' WHERE id = 1;
492492
connect con1,localhost,root;
493493
connection con1;
494-
SET innodb_lock_wait_timeout= 1;
494+
SET innodb_lock_wait_timeout= 0;
495495
DELETE FROM matchmaking_groups WHERE id = 10;
496496
connection default;
497497
COMMIT;
@@ -531,9 +531,10 @@ connection con1;
531531
BEGIN;
532532
UPDATE t2 SET f = 11 WHERE id = 1;
533533
connection default;
534-
SET innodb_lock_wait_timeout= 1;
534+
SET innodb_lock_wait_timeout= 0;
535535
DELETE FROM t1 WHERE id = 1;
536536
ERROR HY000: Lock wait timeout exceeded; try restarting transaction
537+
SET innodb_lock_wait_timeout= 1;
537538
connection con1;
538539
COMMIT;
539540
connection default;
@@ -897,3 +898,30 @@ create or replace table t2 (pk int primary key, a varchar(4096) unique, foreign
897898
ERROR HY000: Can't create table `test`.`t2` (errno: 150 "Foreign key constraint is incorrectly formed")
898899
drop table t1;
899900
# End of 10.5 tests
901+
#
902+
# MDEV-26554 Table-rebuilding DDL on parent table causes crash
903+
# for INSERT into child table
904+
#
905+
CREATE TABLE parent(a INT PRIMARY KEY) ENGINE=InnoDB;
906+
CREATE TABLE child(a INT PRIMARY KEY REFERENCES parent(a)) ENGINE=InnoDB;
907+
connect con1, localhost, root,,;
908+
BEGIN;
909+
INSERT INTO child SET a=1;
910+
ERROR 23000: Cannot add or update a child row: a foreign key constraint fails (`test`.`child`, CONSTRAINT `child_ibfk_1` FOREIGN KEY (`a`) REFERENCES `parent` (`a`))
911+
connection default;
912+
SET innodb_lock_wait_timeout=0, foreign_key_checks=0;
913+
TRUNCATE TABLE parent;
914+
ERROR HY000: Lock wait timeout exceeded; try restarting transaction
915+
ALTER TABLE parent FORCE, ALGORITHM=COPY;
916+
ERROR HY000: Lock wait timeout exceeded; try restarting transaction
917+
ALTER TABLE parent FORCE, ALGORITHM=INPLACE;
918+
ERROR HY000: Lock wait timeout exceeded; try restarting transaction
919+
ALTER TABLE parent ADD COLUMN b INT, ALGORITHM=INSTANT;
920+
ERROR HY000: Lock wait timeout exceeded; try restarting transaction
921+
disconnect con1;
922+
TRUNCATE TABLE parent;
923+
ALTER TABLE parent FORCE, ALGORITHM=COPY;
924+
ALTER TABLE parent FORCE, ALGORITHM=INPLACE;
925+
ALTER TABLE parent ADD COLUMN b INT, ALGORITHM=INSTANT;
926+
DROP TABLE child, parent;
927+
# End of 10.6 tests

mysql-test/suite/innodb/r/row_format_redundant.result

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ DROP TABLE t1;
7575
Warnings:
7676
Warning 1932 Table 'test.t1' doesn't exist in engine
7777
DROP TABLE t2,t3;
78-
FOUND 5 /\[ERROR\] InnoDB: Table test/t1 in InnoDB data dictionary contains invalid flags\. SYS_TABLES\.TYPE=1 SYS_TABLES\.MIX_LEN=511\b/ in mysqld.1.err
78+
FOUND 6 /\[ERROR\] InnoDB: Table test/t1 in InnoDB data dictionary contains invalid flags\. SYS_TABLES\.TYPE=1 SYS_TABLES\.MIX_LEN=511\b/ in mysqld.1.err
7979
# restart
8080
ib_buffer_pool
8181
ib_logfile0

mysql-test/suite/innodb/r/truncate_foreign.result

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,16 +43,19 @@ SET DEBUG_SYNC='foreign_constraint_check_for_ins SIGNAL fk WAIT_FOR go';
4343
INSERT INTO child SET a=5;
4444
connection default;
4545
SET DEBUG_SYNC='now WAIT_FOR fk';
46-
SET foreign_key_checks=0;
46+
SET foreign_key_checks=0, innodb_lock_wait_timeout=0;
4747
TRUNCATE TABLE parent;
48+
ERROR HY000: Lock wait timeout exceeded; try restarting transaction
4849
SET DEBUG_SYNC='now SIGNAL go';
4950
connection dml;
50-
ERROR 23000: Cannot add or update a child row: a foreign key constraint fails (`test`.`child`, CONSTRAINT `child_ibfk_1` FOREIGN KEY (`a`) REFERENCES `parent` (`a`) ON UPDATE CASCADE)
5151
SELECT * FROM parent;
5252
a
53+
3
54+
5
5355
SELECT * FROM child;
5456
a
5557
3
58+
5
5659
disconnect dml;
5760
connection default;
5861
SET DEBUG_SYNC = RESET;

mysql-test/suite/innodb/t/foreign_key.test

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -411,8 +411,8 @@ INSERT INTO t1 VALUES (1,2);
411411
--error ER_XAER_RMFAIL
412412
CREATE TABLE x AS SELECT * FROM t1;
413413
--connect (con1,localhost,root,,test)
414-
SET foreign_key_checks= OFF, innodb_lock_wait_timeout= 1;
415-
SET lock_wait_timeout=5;
414+
SET foreign_key_checks= OFF, innodb_lock_wait_timeout= 0;
415+
SET lock_wait_timeout=2;
416416
--error ER_LOCK_WAIT_TIMEOUT
417417
ALTER TABLE t1 ADD FOREIGN KEY f (a) REFERENCES t1 (pk), LOCK=EXCLUSIVE;# Cleanup
418418
--disconnect con1
@@ -506,7 +506,7 @@ UPDATE users SET name = 'qux' WHERE id = 1;
506506

507507
connect (con1,localhost,root);
508508
--connection con1
509-
SET innodb_lock_wait_timeout= 1;
509+
SET innodb_lock_wait_timeout= 0;
510510
DELETE FROM matchmaking_groups WHERE id = 10;
511511

512512
--connection default
@@ -544,9 +544,10 @@ BEGIN;
544544
UPDATE t2 SET f = 11 WHERE id = 1;
545545

546546
--connection default
547-
SET innodb_lock_wait_timeout= 1;
547+
SET innodb_lock_wait_timeout= 0;
548548
--error ER_LOCK_WAIT_TIMEOUT
549549
DELETE FROM t1 WHERE id = 1;
550+
SET innodb_lock_wait_timeout= 1;
550551

551552
--connection con1
552553
COMMIT;
@@ -902,4 +903,34 @@ drop table t1;
902903

903904
--echo # End of 10.5 tests
904905

906+
--echo #
907+
--echo # MDEV-26554 Table-rebuilding DDL on parent table causes crash
908+
--echo # for INSERT into child table
909+
--echo #
910+
911+
CREATE TABLE parent(a INT PRIMARY KEY) ENGINE=InnoDB;
912+
CREATE TABLE child(a INT PRIMARY KEY REFERENCES parent(a)) ENGINE=InnoDB;
913+
connect (con1, localhost, root,,);
914+
BEGIN;
915+
--error ER_NO_REFERENCED_ROW_2
916+
INSERT INTO child SET a=1;
917+
connection default;
918+
SET innodb_lock_wait_timeout=0, foreign_key_checks=0;
919+
--error ER_LOCK_WAIT_TIMEOUT
920+
TRUNCATE TABLE parent;
921+
--error ER_LOCK_WAIT_TIMEOUT
922+
ALTER TABLE parent FORCE, ALGORITHM=COPY;
923+
--error ER_LOCK_WAIT_TIMEOUT
924+
ALTER TABLE parent FORCE, ALGORITHM=INPLACE;
925+
--error ER_LOCK_WAIT_TIMEOUT
926+
ALTER TABLE parent ADD COLUMN b INT, ALGORITHM=INSTANT;
927+
disconnect con1;
928+
TRUNCATE TABLE parent;
929+
ALTER TABLE parent FORCE, ALGORITHM=COPY;
930+
ALTER TABLE parent FORCE, ALGORITHM=INPLACE;
931+
ALTER TABLE parent ADD COLUMN b INT, ALGORITHM=INSTANT;
932+
DROP TABLE child, parent;
933+
934+
--echo # End of 10.6 tests
935+
905936
--source include/wait_until_count_sessions.inc

mysql-test/suite/innodb/t/truncate_foreign.test

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,12 +52,12 @@ send INSERT INTO child SET a=5;
5252

5353
connection default;
5454
SET DEBUG_SYNC='now WAIT_FOR fk';
55-
SET foreign_key_checks=0;
55+
SET foreign_key_checks=0, innodb_lock_wait_timeout=0;
56+
--error ER_LOCK_WAIT_TIMEOUT
5657
TRUNCATE TABLE parent;
5758
SET DEBUG_SYNC='now SIGNAL go';
5859

5960
connection dml;
60-
--error ER_NO_REFERENCED_ROW_2
6161
reap;
6262
SELECT * FROM parent;
6363
SELECT * FROM child;

storage/innobase/handler/ha_innodb.cc

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13645,7 +13645,6 @@ static dberr_t innobase_rename_table(trx_t *trx, const char *from,
1364513645

1364613646
DEBUG_SYNC_C("innodb_rename_table_ready");
1364713647

13648-
trx_start_if_not_started(trx, true);
1364913648
ut_ad(trx->will_lock);
1365013649

1365113650
error = row_rename_table_for_mysql(norm_from, norm_to, trx, use_fk);
@@ -13782,7 +13781,23 @@ int ha_innobase::truncate()
1378213781
dict_table_t *table_stats = nullptr, *index_stats = nullptr;
1378313782
MDL_ticket *mdl_table = nullptr, *mdl_index = nullptr;
1378413783

13785-
dberr_t error = lock_table_for_trx(ib_table, trx, LOCK_X);
13784+
dberr_t error = DB_SUCCESS;
13785+
13786+
dict_sys.freeze(SRW_LOCK_CALL);
13787+
for (const dict_foreign_t* f : ib_table->referenced_set) {
13788+
if (dict_table_t* child = f->foreign_table) {
13789+
error = lock_table_for_trx(child, trx, LOCK_X);
13790+
if (error != DB_SUCCESS) {
13791+
break;
13792+
}
13793+
}
13794+
}
13795+
dict_sys.unfreeze();
13796+
13797+
if (error == DB_SUCCESS) {
13798+
error = lock_table_for_trx(ib_table, trx, LOCK_X);
13799+
}
13800+
1378613801
const bool fts = error == DB_SUCCESS
1378713802
&& ib_table->flags2 & (DICT_TF2_FTS_HAS_DOC_ID | DICT_TF2_FTS);
1378813803

@@ -13945,6 +13960,26 @@ ha_innobase::rename_table(
1394513960

1394613961
dberr_t error = DB_SUCCESS;
1394713962

13963+
if (dict_table_t::is_temporary_name(norm_from)) {
13964+
/* There is no need to lock any FOREIGN KEY child tables. */
13965+
} else if (dict_table_t *table = dict_table_open_on_name(
13966+
norm_from, false, DICT_ERR_IGNORE_FK_NOKEY)) {
13967+
dict_sys.freeze(SRW_LOCK_CALL);
13968+
for (const dict_foreign_t* f : table->referenced_set) {
13969+
if (dict_table_t* child = f->foreign_table) {
13970+
error = lock_table_for_trx(child, trx, LOCK_X);
13971+
if (error != DB_SUCCESS) {
13972+
break;
13973+
}
13974+
}
13975+
}
13976+
dict_sys.unfreeze();
13977+
if (error == DB_SUCCESS) {
13978+
error = lock_table_for_trx(table, trx, LOCK_X);
13979+
}
13980+
table->release();
13981+
}
13982+
1394813983
if (strcmp(norm_from, TABLE_STATS_NAME)
1394913984
&& strcmp(norm_from, INDEX_STATS_NAME)
1395013985
&& strcmp(norm_to, TABLE_STATS_NAME)
@@ -13966,7 +14001,7 @@ ha_innobase::rename_table(
1396614001
dict_sys.unfreeze();
1396714002
}
1396814003

13969-
if (table_stats && index_stats
14004+
if (error == DB_SUCCESS && table_stats && index_stats
1397014005
&& !strcmp(table_stats->name.m_name, TABLE_STATS_NAME)
1397114006
&& !strcmp(index_stats->name.m_name, INDEX_STATS_NAME) &&
1397214007
!(error = lock_table_for_trx(table_stats, trx, LOCK_X))) {

storage/innobase/handler/handler0alter.cc

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10847,12 +10847,24 @@ ha_innobase::commit_inplace_alter_table(
1084710847

1084810848
for (inplace_alter_handler_ctx** pctx = ctx_array; *pctx; pctx++) {
1084910849
auto ctx = static_cast<ha_innobase_inplace_ctx*>(*pctx);
10850+
dberr_t error = DB_SUCCESS;
1085010851

1085110852
if (new_clustered && ctx->old_table->fts) {
1085210853
ut_ad(!ctx->old_table->fts->add_wq);
1085310854
fts_optimize_remove_table(ctx->old_table);
1085410855
}
1085510856

10857+
dict_sys.freeze(SRW_LOCK_CALL);
10858+
for (auto f : ctx->old_table->referenced_set) {
10859+
if (dict_table_t* child = f->foreign_table) {
10860+
error = lock_table_for_trx(child, trx, LOCK_X);
10861+
if (error != DB_SUCCESS) {
10862+
break;
10863+
}
10864+
}
10865+
}
10866+
dict_sys.unfreeze();
10867+
1085610868
if (ctx->new_table->fts) {
1085710869
ut_ad(!ctx->new_table->fts->add_wq);
1085810870
fts_optimize_remove_table(ctx->new_table);
@@ -10863,9 +10875,12 @@ ha_innobase::commit_inplace_alter_table(
1086310875
transaction is holding locks on the table while we
1086410876
change the table definition. Any recovered incomplete
1086510877
transactions would be holding InnoDB locks only, not MDL. */
10878+
if (error == DB_SUCCESS) {
10879+
error = lock_table_for_trx(ctx->new_table, trx,
10880+
LOCK_X);
10881+
}
1086610882

10867-
if (dberr_t error = lock_table_for_trx(ctx->new_table, trx,
10868-
LOCK_X)) {
10883+
if (error != DB_SUCCESS) {
1086910884
lock_fail:
1087010885
my_error_innodb(
1087110886
error, table_share->table_name.str, 0);

0 commit comments

Comments
 (0)