From 9262a7aa70c467399f0ade9a5d81705452cd073d Mon Sep 17 00:00:00 2001 From: Hemant Dangi Date: Fri, 3 Oct 2025 11:16:26 +0530 Subject: [PATCH] MDL BF-BF conflict on ALTER and INSERT with multi-level foreign key parents Issue: On galera write node INSERT statements does not acquire MDL locks on it's all child tables and thereby wsrep certification keys are also added for limited tables, but on applier nodes it does acquire MDL locks for all child tables. This can result into MDL BF-BF conflict on applier node when transactions referring to parent and child tables are executed concurrently. For example: Tables with foreign keys: t1<-t2<-t3<-t4 Conflicting transactions: INSERT t1 and DROP TABLE t4 Wsrep certification keys taken on write node: - for INSERT t1: t1 and t2 - for DROP TABLE t4: t4 On applier node MDL BF-BF conflict happened between two transaction because MDL locks on t1, t2, t3 and t4 were taken for INSERT t1, which conflicted with MDL lock on t4 taken by DROP TABLE t4. The Wsrep certification keys helps in resolving this MDL BF-BF conflict by prioritizing and scheduling concurrent transactions. But to generate Wsrep certification keys it needs to open and take MDL locks on all the child tables. On applier nodes Write_rows event is implicitly a REPLACE, deleting all conflicting rows which can cause cascading FK actions and locks on foreign key children tables. Solution: For Galera applier nodes the Write_rows event is considered pure INSERT which will never cause cascading FK actions and locks on foreign key children tables. --- .../r/galera_multi_level_fk_ddl_insert.result | 22 --- .../t/galera_multi_level_fk_ddl_insert.test | 137 +----------------- .../galera_multi_level_foreign_key_insert.inc | 44 ++++++ sql/log_event_server.cc | 16 +- sql/sql_base.cc | 45 +----- sql/table.h | 6 +- sql/wsrep_mysqld.cc | 2 +- 7 files changed, 70 insertions(+), 202 deletions(-) create mode 100644 mysql-test/suite/galera/t/galera_multi_level_foreign_key_insert.inc diff --git a/mysql-test/suite/galera/r/galera_multi_level_fk_ddl_insert.result b/mysql-test/suite/galera/r/galera_multi_level_fk_ddl_insert.result index aa120e87cb9af..f799d372484a7 100644 --- a/mysql-test/suite/galera/r/galera_multi_level_fk_ddl_insert.result +++ b/mysql-test/suite/galera/r/galera_multi_level_fk_ddl_insert.result @@ -48,7 +48,6 @@ connection node_2; SET DEBUG_SYNC = "now WAIT_FOR sync.wsrep_apply_toi_reached"; SET SESSION wsrep_sync_wait = 0; connection node_1; -SET GLOBAL DEBUG_DBUG = '+d,wsrep_print_foreign_keys_table'; START TRANSACTION; INSERT INTO t1 VALUES (3,0); COMMIT; @@ -62,10 +61,6 @@ connection node_1; SET DEBUG_SYNC = 'RESET'; SET GLOBAL DEBUG_DBUG = ""; SET GLOBAL wsrep_slave_threads=DEFAULT; -connection node_1; -include/assert_grep.inc [Foreign key referenced table found: 2 tables] -include/assert_grep.inc [Foreign key referenced table found: test.t2] -include/assert_grep.inc [Foreign key referenced table found: test.t3] connection node_2; select * from t1; id f2 @@ -146,7 +141,6 @@ connection node_2; SET DEBUG_SYNC = "now WAIT_FOR sync.wsrep_apply_toi_reached"; SET SESSION wsrep_sync_wait = 0; connection node_1; -SET GLOBAL DEBUG_DBUG = '+d,wsrep_print_foreign_keys_table'; START TRANSACTION; INSERT INTO t1 VALUES (3,0); COMMIT; @@ -160,11 +154,6 @@ connection node_1; SET DEBUG_SYNC = 'RESET'; SET GLOBAL DEBUG_DBUG = ""; SET GLOBAL wsrep_slave_threads=DEFAULT; -connection node_1; -include/assert_grep.inc [Foreign key referenced table found: 3 tables] -include/assert_grep.inc [Foreign key referenced table found: test.t2] -include/assert_grep.inc [Foreign key referenced table found: test.t3] -include/assert_grep.inc [Foreign key referenced table found: test.t4] connection node_2; select * from t1; id f2 @@ -233,7 +222,6 @@ connection node_2; SET DEBUG_SYNC = "now WAIT_FOR sync.wsrep_apply_toi_reached"; SET SESSION wsrep_sync_wait = 0; connection node_1; -SET GLOBAL DEBUG_DBUG = '+d,wsrep_print_foreign_keys_table'; START TRANSACTION; INSERT INTO t1 VALUES (3,0); COMMIT; @@ -247,11 +235,6 @@ connection node_1; SET DEBUG_SYNC = 'RESET'; SET GLOBAL DEBUG_DBUG = ""; SET GLOBAL wsrep_slave_threads=DEFAULT; -connection node_1; -include/assert_grep.inc [Foreign key referenced table found: 2 tables] -include/assert_grep.inc [Foreign key referenced table found: test.t2] -include/assert_grep.inc [Foreign key referenced table found: test.t3] -include/assert_grep.inc [Foreign key referenced table found: test.t4] connection node_2; select * from t1; id f2 @@ -323,7 +306,6 @@ connection node_2; SET DEBUG_SYNC = "now WAIT_FOR sync.wsrep_apply_toi_reached"; SET SESSION wsrep_sync_wait = 0; connection node_1; -SET GLOBAL DEBUG_DBUG = '+d,wsrep_print_foreign_keys_table'; START TRANSACTION; INSERT INTO t1 VALUES (3,0); COMMIT; @@ -337,10 +319,6 @@ connection node_1; SET DEBUG_SYNC = 'RESET'; SET GLOBAL DEBUG_DBUG = ""; SET GLOBAL wsrep_slave_threads=DEFAULT; -connection node_1; -include/assert_grep.inc [Foreign key referenced table found: 1 tables] -include/assert_grep.inc [Foreign key referenced table found: test.t2] -include/assert_grep.inc [Foreign key referenced table found: test.t3] connection node_2; select * from t1; id f2 diff --git a/mysql-test/suite/galera/t/galera_multi_level_fk_ddl_insert.test b/mysql-test/suite/galera/t/galera_multi_level_fk_ddl_insert.test index d4a1874d51aab..36fcb3947d51d 100644 --- a/mysql-test/suite/galera/t/galera_multi_level_fk_ddl_insert.test +++ b/mysql-test/suite/galera/t/galera_multi_level_fk_ddl_insert.test @@ -72,35 +72,8 @@ INSERT INTO t4 VALUES (2,2,1234); --let $fk_parent_query = DROP TABLE t4 --let $fk_child_query = INSERT INTO t1 VALUES (3,0) ---let $fk_mdl_lock_num = 3 ---source galera_multi_level_foreign_key.inc - - -# -# Verify Foreign key for referenced table added. -# ---connection node_1 ---let assert_text= Foreign key referenced table found: 2 tables ---let $assert_file= $MYSQLTEST_VARDIR/log/mysqld.1.err ---let assert_count= 2 ---let assert_select= Foreign key referenced table found: ---let assert_only_after= CURRENT_TEST: galera.galera_multi_level_fk_ddl_insert ---source include/assert_grep.inc - ---let assert_text= Foreign key referenced table found: test.t2 ---let $assert_file= $MYSQLTEST_VARDIR/log/mysqld.1.err ---let assert_count= 1 ---let assert_select= Foreign key referenced table found: test.t2 ---let assert_only_after= CURRENT_TEST: galera.galera_multi_level_fk_ddl_insert ---source include/assert_grep.inc - ---let assert_text= Foreign key referenced table found: test.t3 ---let $assert_file= $MYSQLTEST_VARDIR/log/mysqld.1.err ---let assert_count= 1 ---let assert_select= Foreign key referenced table found: test.t3 ---let assert_only_after= CURRENT_TEST: galera.galera_multi_level_fk_ddl_insert ---source include/assert_grep.inc - +--let $fk_mdl_lock_num = 2 +--source galera_multi_level_foreign_key_insert.inc # # Verify insert and drop table has succeded. @@ -191,41 +164,8 @@ INSERT INTO t4 VALUES (2,2,1234); # Issue a INSERT to table that references t1 # --let $fk_child_query = INSERT INTO t1 VALUES (3,0) ---let $fk_mdl_lock_num = 4 ---source galera_multi_level_foreign_key.inc - - -# -# Verify Foreign key for referenced table added. -# ---connection node_1 ---let assert_text= Foreign key referenced table found: 3 tables ---let $assert_file= $MYSQLTEST_VARDIR/log/mysqld.1.err ---let assert_count= 5 ---let assert_select= Foreign key referenced table found: ---let assert_only_after= CURRENT_TEST: galera.galera_multi_level_fk_ddl_insert ---source include/assert_grep.inc - ---let assert_text= Foreign key referenced table found: test.t2 ---let $assert_file= $MYSQLTEST_VARDIR/log/mysqld.1.err ---let assert_count= 2 ---let assert_select= Foreign key referenced table found: test.t2 ---let assert_only_after= CURRENT_TEST: galera.galera_multi_level_fk_ddl_insert ---source include/assert_grep.inc - ---let assert_text= Foreign key referenced table found: test.t3 ---let $assert_file= $MYSQLTEST_VARDIR/log/mysqld.1.err ---let assert_count= 2 ---let assert_select= Foreign key referenced table found: test.t3 ---let assert_only_after= CURRENT_TEST: galera.galera_multi_level_fk_ddl_insert ---source include/assert_grep.inc - ---let assert_text= Foreign key referenced table found: test.t4 ---let $assert_file= $MYSQLTEST_VARDIR/log/mysqld.1.err ---let assert_count= 1 ---let assert_select= Foreign key referenced table found: test.t4 ---let assert_only_after= CURRENT_TEST: galera.galera_multi_level_fk_ddl_insert ---source include/assert_grep.inc +--let $fk_mdl_lock_num = 2 +--source galera_multi_level_foreign_key_insert.inc # @@ -296,42 +236,8 @@ INSERT INTO t3 VALUES (2,2,1234); --let $fk_parent_query = CREATE TABLE t4 (id INT PRIMARY KEY, t3_id INT NOT NULL, f2 INTEGER NOT NULL, KEY key_t3_id(t3_id), CONSTRAINT key_t3_id FOREIGN KEY (t3_id) REFERENCES t3 (id) ON UPDATE CASCADE ON DELETE CASCADE) --let $fk_child_query = INSERT INTO t1 VALUES (3,0) ---let $fk_mdl_lock_num = 4 ---source galera_multi_level_foreign_key.inc - - -# -# Verify Foreign key for referenced table added. -# ---connection node_1 ---let assert_text= Foreign key referenced table found: 2 tables ---let $assert_file= $MYSQLTEST_VARDIR/log/mysqld.1.err ---let assert_count= 8 ---let assert_select= Foreign key referenced table found: ---let $assert_only_after = CURRENT_TEST: ---let assert_only_after= CURRENT_TEST: galera.galera_multi_level_fk_ddl_insert ---source include/assert_grep.inc - ---let assert_text= Foreign key referenced table found: test.t2 ---let $assert_file= $MYSQLTEST_VARDIR/log/mysqld.1.err ---let assert_count= 3 ---let assert_select= Foreign key referenced table found: test.t2 ---let assert_only_after= CURRENT_TEST: galera.galera_multi_level_fk_ddl_insert ---source include/assert_grep.inc - ---let assert_text= Foreign key referenced table found: test.t3 ---let $assert_file= $MYSQLTEST_VARDIR/log/mysqld.1.err ---let assert_count= 3 ---let assert_select= Foreign key referenced table found: test.t3 ---let assert_only_after= CURRENT_TEST: galera.galera_multi_level_fk_ddl_insert ---source include/assert_grep.inc - ---let assert_text= Foreign key referenced table found: test.t4 ---let $assert_file= $MYSQLTEST_VARDIR/log/mysqld.1.err ---let assert_count= 2 ---let assert_select= Foreign key referenced table found: test.t4 ---let assert_only_after= CURRENT_TEST: galera.galera_multi_level_fk_ddl_insert ---source include/assert_grep.inc +--let $fk_mdl_lock_num = 2 +--source galera_multi_level_foreign_key_insert.inc # @@ -403,35 +309,8 @@ INSERT INTO t3 VALUES (2,2,1234); --let $fk_parent_query = OPTIMIZE TABLE t3 --let $fk_child_query = INSERT INTO t1 VALUES (3,0) ---let $fk_mdl_lock_num = 3 ---source galera_multi_level_foreign_key.inc - - -# -# Verify Foreign key for referenced table added. -# ---connection node_1 ---let assert_text= Foreign key referenced table found: 1 tables ---let $assert_file= $MYSQLTEST_VARDIR/log/mysqld.1.err ---let assert_count= 10 ---let assert_select= Foreign key referenced table found: ---let $assert_only_after = CURRENT_TEST: ---let assert_only_after= CURRENT_TEST: galera.galera_multi_level_fk_ddl_insert ---source include/assert_grep.inc - ---let assert_text= Foreign key referenced table found: test.t2 ---let $assert_file= $MYSQLTEST_VARDIR/log/mysqld.1.err ---let assert_count= 4 ---let assert_select= Foreign key referenced table found: test.t2 ---let assert_only_after= CURRENT_TEST: galera.galera_multi_level_fk_ddl_insert ---source include/assert_grep.inc - ---let assert_text= Foreign key referenced table found: test.t3 ---let $assert_file= $MYSQLTEST_VARDIR/log/mysqld.1.err ---let assert_count= 4 ---let assert_select= Foreign key referenced table found: test.t3 ---let assert_only_after= CURRENT_TEST: galera.galera_multi_level_fk_ddl_insert ---source include/assert_grep.inc +--let $fk_mdl_lock_num = 2 +--source galera_multi_level_foreign_key_insert.inc # diff --git a/mysql-test/suite/galera/t/galera_multi_level_foreign_key_insert.inc b/mysql-test/suite/galera/t/galera_multi_level_foreign_key_insert.inc new file mode 100644 index 0000000000000..5cb79bd474136 --- /dev/null +++ b/mysql-test/suite/galera/t/galera_multi_level_foreign_key_insert.inc @@ -0,0 +1,44 @@ + +--connection node_2 +SET GLOBAL DEBUG_DBUG = '+d,sync.wsrep_apply_toi'; + +--connection node_1 +--eval $fk_parent_query + +--connection node_2 +SET DEBUG_SYNC = "now WAIT_FOR sync.wsrep_apply_toi_reached"; +SET SESSION wsrep_sync_wait = 0; + +# +# Execute child query on node_1. +# If bug is present, expect the wait condition +# to timeout and when the child query applies, it +# will be granted a MDL lock on parent table. +# When resumed, the parent query will +# also try to acquire MDL lock on parent table, +# causing a BF-BF conflict on that MDL lock. +# +--connection node_1 +START TRANSACTION; +--eval $fk_child_query +--let $wait_condition = SELECT COUNT(*) = $fk_mdl_lock_num FROM performance_schema.metadata_locks WHERE OBJECT_SCHEMA='test' AND LOCK_STATUS="GRANTED" +--source include/wait_condition.inc +COMMIT; + + +--connection node_2 +SET GLOBAL DEBUG_DBUG = '-d,sync.wsrep_apply_toi'; +SET DEBUG_SYNC = "now SIGNAL signal.wsrep_apply_toi"; + + +# +# Cleanup +# +SET DEBUG_SYNC = 'RESET'; +SET GLOBAL DEBUG_DBUG = ""; +SET GLOBAL wsrep_slave_threads=DEFAULT; + +--connection node_1 +SET DEBUG_SYNC = 'RESET'; +SET GLOBAL DEBUG_DBUG = ""; +SET GLOBAL wsrep_slave_threads=DEFAULT; diff --git a/sql/log_event_server.cc b/sql/log_event_server.cc index 9ad2c5ebb1e28..5beccb1e88c52 100644 --- a/sql/log_event_server.cc +++ b/sql/log_event_server.cc @@ -8001,11 +8001,23 @@ Write_rows_log_event::do_exec_row(rpl_group_info *rgi) #if defined(HAVE_REPLICATION) uint8 Write_rows_log_event::get_trg_event_map() { - return trg2bit(TRG_EVENT_INSERT) | trg2bit(TRG_EVENT_UPDATE) | - trg2bit(TRG_EVENT_DELETE); + /* + In SLAVE_EXEC_MODE_IDEMPOTENT mode, Write_rows_log_event event is + implicitly a REPLACE, deleting all conflicting rows which can cause + foreign key constraint cascade operations on FK referencing table. + + In SLAVE_EXEC_MODE_STRICT mode, the Write_rows_log_event is pure INSERT, + will never cause foreign key constraint cascade operations on foreign key + referencing tables. + */ + if (slave_exec_mode_options == SLAVE_EXEC_MODE_IDEMPOTENT) + return trg2bit(TRG_EVENT_INSERT) | trg2bit(TRG_EVENT_DELETE); + else + return trg2bit(TRG_EVENT_INSERT); } #endif + /************************************************************************** Delete_rows_log_event member functions **************************************************************************/ diff --git a/sql/sql_base.cc b/sql/sql_base.cc index 34d4098879384..b9d6497d2ce2d 100644 --- a/sql/sql_base.cc +++ b/sql/sql_base.cc @@ -3965,15 +3965,6 @@ bool extend_table_list(THD *thd, TABLE_LIST *tables, (tables->updating && tables->lock_type >= TL_FIRST_WRITE) || thd->lex->default_used; -#ifdef WITH_WSREP - if (WSREP(thd) && !thd->wsrep_applier && - wsrep_is_active(thd) && - (sql_command_flags[thd->lex->sql_command] & CF_INSERTS_DATA) && - tables->lock_type == TL_READ) { - maybe_need_prelocking= true; - } -#endif - if (thd->locked_tables_mode <= LTM_LOCK_TABLES && ! has_prelocking_list && maybe_need_prelocking) { @@ -5106,7 +5097,6 @@ prepare_fk_prelocking_list(THD *thd, Query_tables_list *prelocking_ctx, Query_arena *arena, backup; TABLE *table= table_list->table; bool error= FALSE; - bool override_fk_ignore_table= FALSE; if (!table->file->referenced_by_foreign_key()) DBUG_RETURN(FALSE); @@ -5123,37 +5113,6 @@ prepare_fk_prelocking_list(THD *thd, Query_tables_list *prelocking_ctx, *need_prelocking= TRUE; -#ifdef WITH_WSREP - /* - MDL is enough for read-only FK checks, we don't need the table, - but on galera applier node lock_type is set to TL_FIRST_WRITE and not - TL_READ, which is due to Write_rows_log_event event logged for INSERT - is used to record insert, update and delete - (Write_rows_log_event::get_trg_event_map()), and therefore all child - tables will be opened and MDL locks will be taken on applier node, while - opening of multiple child tables is ignored on write node by setting - open_strategy= OPEN_STUB in init_one_table_for_prelocking(). - This difference in write and applier node can result in MDL deadlock. - Tables with foreign keys: t1<-t2<-t3<-t4 - Conflicting transactions: INSERT t1 and DROP TABLE t4 - Wsrep certification keys taken on write node: - - for INSERT t1: t1 and t2 - - for DROP TABLE t4: t4 - On applier node MDL deadlock happened between two transaction because - MDL locks for INSERT t1 were taken on t1, t2, t3 and t4, which conflicted - with MDL lock on t4 taken by DROP TABLE t4. - The Wsrep certification keys does helps in resolving in transactions - getting MDL deadlock. But to generate Wsrep certification keys it needs - to open and take MDL locks on all child tables. So that conflicting - transactions can be prioritize and scheduled. - */ - if (WSREP(thd) && !thd->wsrep_applier && - wsrep_is_active(thd) && - (sql_command_flags[thd->lex->sql_command] & CF_INSERTS_DATA)) { - override_fk_ignore_table= TRUE; - } -#endif // WITH_WSREP - while ((fk= fk_list_it++)) { // FK_OPTION_RESTRICT and FK_OPTION_NO_ACTION only need read access @@ -5177,15 +5136,13 @@ prepare_fk_prelocking_list(THD *thd, Query_tables_list *prelocking_ctx, TABLE_LIST::PRELOCK_FK, table_list->belong_to_view, op, &prelocking_ctx->query_tables_last, - table_list->for_insert_data, - override_fk_ignore_table); + table_list->for_insert_data); #ifdef WITH_WSREP /* Append table level shared key for the referenced/foreign table for: - statement that updates existing rows (UPDATE, multi-update) - statement that deletes existing rows (DELETE, DELETE_MULTI) - - statement that inserts new rows (INSERT, REPLACE, LOAD, ALTER TABLE) This is done to avoid potential MDL conflicts with concurrent DDLs. */ if (wsrep_foreign_key_append(thd, fk)) diff --git a/sql/table.h b/sql/table.h index 341532b634545..d44ceaebd7971 100644 --- a/sql/table.h +++ b/sql/table.h @@ -2383,8 +2383,7 @@ struct TABLE_LIST const LEX_CSTRING *table_name_arg, const LEX_CSTRING *alias_arg, enum thr_lock_type lock_type_arg, prelocking_types prelocking_type, TABLE_LIST *belong_to_view_arg, uint8 trg_event_map_arg, - TABLE_LIST ***last_ptr, my_bool insert_data, - my_bool override_fk_ignore_table= FALSE) + TABLE_LIST ***last_ptr, my_bool insert_data) { init_one_table(db_arg, table_name_arg, alias_arg, lock_type_arg); cacheable_table= 1; @@ -2395,8 +2394,7 @@ struct TABLE_LIST belong_to_view= belong_to_view_arg; trg_event_map= trg_event_map_arg; /* MDL is enough for read-only FK checks, we don't need the table */ - if (prelocking_type == PRELOCK_FK && lock_type < TL_FIRST_WRITE && - !override_fk_ignore_table) + if (prelocking_type == PRELOCK_FK && lock_type < TL_FIRST_WRITE) open_strategy= OPEN_STUB; **last_ptr= this; diff --git a/sql/wsrep_mysqld.cc b/sql/wsrep_mysqld.cc index a5840875ff73a..bb6e491feddf2 100644 --- a/sql/wsrep_mysqld.cc +++ b/sql/wsrep_mysqld.cc @@ -4127,7 +4127,7 @@ bool wsrep_foreign_key_append(THD *thd, FOREIGN_KEY_INFO *fk) if (WSREP(thd) && !thd->wsrep_applier && wsrep_is_active(thd) && (sql_command_flags[thd->lex->sql_command] & - (CF_UPDATES_DATA | CF_DELETES_DATA | CF_INSERTS_DATA))) + (CF_UPDATES_DATA | CF_DELETES_DATA))) { wsrep::key key(wsrep::key::shared); key.append_key_part(fk->foreign_db->str, fk->foreign_db->length);