From 40b07e07c3cb285110ac83cfeec50e46b24bc282 Mon Sep 17 00:00:00 2001 From: Thirunarayanan Balathandayuthapani Date: Fri, 16 Jan 2026 17:01:46 +0530 Subject: [PATCH] MDEV-36493 Atomic CREATE OR REPLACE ... SELECT blocks InnoDB purge dict_table_t::parse_tbl_name() : Extracts database and table components from full table names ddl_log_acquire_mdl(): Does take MDL on given table - DELETE/REPLACE: single table MDL acquisition - RENAME: source and destination table MDL protection - ALTER: comprehensive MDL on original, intermediate and final tables - EXCHANGE: primary, source, and temporary table MDL coverage MDL acquistion on temporary tables in mysql_alter_table(), crash recovery for DDL log entry. innodb_mdl_acquire() for temporary tables in commit_inplace_alter_table() innodb_mdl_check() to check MDL ownership verifiction in rename_table() and delete_table(). In innodb_rename_table(), it does check for source and destination table MDL check - InnoDB purge thread now takes MDL on table name directly instead of relying on mdl_name --- mysql-test/main/backup_lock.result | 30 ++--- mysql-test/main/backup_lock.test | 4 +- sql/ddl_log.cc | 135 ++++++++++++++++++++++- sql/sql_partition_admin.cc | 8 ++ sql/sql_table.cc | 23 ++++ storage/innobase/dict/dict0dict.cc | 122 ++++++++++++++------ storage/innobase/handler/ha_innodb.cc | 82 +++++++++++++- storage/innobase/include/dict0mem.h | 20 +++- storage/innobase/include/ha_prototypes.h | 7 ++ storage/innobase/row/row0mysql.cc | 5 + storage/innobase/trx/trx0purge.cc | 7 +- 11 files changed, 385 insertions(+), 58 deletions(-) diff --git a/mysql-test/main/backup_lock.result b/mysql-test/main/backup_lock.result index 6e2ccad5091ea..cb9323a0469d5 100644 --- a/mysql-test/main/backup_lock.result +++ b/mysql-test/main/backup_lock.result @@ -4,27 +4,27 @@ InnoDB 0 transactions not purged BACKUP STAGE START; SELECT LOCK_MODE, LOCK_TYPE, TABLE_SCHEMA, TABLE_NAME FROM information_schema.metadata_lock_info -WHERE TABLE_NAME NOT LIKE 'innodb_%_stats'; +WHERE TABLE_NAME NOT LIKE 'innodb_%_stats' ORDER BY TABLE_NAME; LOCK_MODE LOCK_TYPE TABLE_SCHEMA TABLE_NAME MDL_BACKUP_START Backup lock BACKUP STAGE FLUSH; SELECT LOCK_MODE, LOCK_TYPE, TABLE_SCHEMA, TABLE_NAME FROM information_schema.metadata_lock_info -WHERE TABLE_NAME NOT LIKE 'innodb_%_stats'; +WHERE TABLE_NAME NOT LIKE 'innodb_%_stats' ORDER BY TABLE_NAME; LOCK_MODE LOCK_TYPE TABLE_SCHEMA TABLE_NAME MDL_BACKUP_FLUSH Backup lock BACKUP STAGE BLOCK_DDL; SELECT LOCK_MODE, LOCK_TYPE, TABLE_SCHEMA, TABLE_NAME FROM information_schema.metadata_lock_info -WHERE TABLE_NAME NOT LIKE 'innodb_%_stats'; +WHERE TABLE_NAME NOT LIKE 'innodb_%_stats' ORDER BY TABLE_NAME; LOCK_MODE LOCK_TYPE TABLE_SCHEMA TABLE_NAME MDL_BACKUP_WAIT_DDL Backup lock BACKUP STAGE BLOCK_COMMIT; SELECT LOCK_MODE, LOCK_TYPE, TABLE_SCHEMA, TABLE_NAME FROM information_schema.metadata_lock_info -WHERE TABLE_NAME NOT LIKE 'innodb_%_stats'; +WHERE TABLE_NAME NOT LIKE 'innodb_%_stats' ORDER BY TABLE_NAME; LOCK_MODE LOCK_TYPE TABLE_SCHEMA TABLE_NAME MDL_BACKUP_WAIT_COMMIT Backup lock BACKUP STAGE END; SELECT LOCK_MODE, LOCK_TYPE, TABLE_SCHEMA, TABLE_NAME FROM information_schema.metadata_lock_info -WHERE TABLE_NAME NOT LIKE 'innodb_%_stats'; +WHERE TABLE_NAME NOT LIKE 'innodb_%_stats' ORDER BY TABLE_NAME; LOCK_MODE LOCK_TYPE TABLE_SCHEMA TABLE_NAME # # testing BACKUP STAGE LOCK's @@ -47,13 +47,14 @@ alter table t1 add column (j int), algorithm copy, lock shared; connection con2; backup stage flush; SELECT LOCK_MODE, LOCK_TYPE, TABLE_SCHEMA, TABLE_NAME FROM information_schema.metadata_lock_info -WHERE TABLE_NAME NOT LIKE 'innodb_%_stats'; +WHERE TABLE_NAME NOT LIKE 'innodb_%_stats' ORDER BY TABLE_NAME; LOCK_MODE LOCK_TYPE TABLE_SCHEMA TABLE_NAME +MDL_INTENTION_EXCLUSIVE Schema metadata lock test MDL_BACKUP_DDL Backup lock MDL_BACKUP_FLUSH Backup lock -MDL_SHARED_WRITE Table metadata lock test t1 +MDL_EXCLUSIVE Table metadata lock test #sql-alter MDL_SHARED_UPGRADABLE Table metadata lock test t1 -MDL_INTENTION_EXCLUSIVE Schema metadata lock test +MDL_SHARED_WRITE Table metadata lock test t1 SET STATEMENT max_statement_time=1 FOR backup stage block_ddl; ERROR 70100: Query execution was interrupted (max_statement_time exceeded) backup stage block_ddl; @@ -84,13 +85,14 @@ connection con2; backup stage start; backup stage flush; SELECT LOCK_MODE, LOCK_TYPE, TABLE_SCHEMA, TABLE_NAME FROM information_schema.metadata_lock_info -WHERE TABLE_NAME NOT LIKE 'innodb_%_stats'; +WHERE TABLE_NAME NOT LIKE 'innodb_%_stats' ORDER BY TABLE_NAME; LOCK_MODE LOCK_TYPE TABLE_SCHEMA TABLE_NAME MDL_BACKUP_ALTER_COPY Backup lock +MDL_INTENTION_EXCLUSIVE Schema metadata lock test MDL_BACKUP_FLUSH Backup lock -MDL_SHARED_WRITE Table metadata lock test t1 +MDL_EXCLUSIVE Table metadata lock test #sql-alter MDL_SHARED_UPGRADABLE Table metadata lock test t1 -MDL_INTENTION_EXCLUSIVE Schema metadata lock test +MDL_SHARED_WRITE Table metadata lock test t1 backup stage block_ddl; backup stage block_commit; connection default; @@ -121,11 +123,11 @@ SET STATEMENT lock_wait_timeout=0 FOR SELECT * FROM t1; ERROR HY000: Lock wait timeout exceeded; try restarting transaction backup stage block_ddl; SELECT LOCK_MODE, LOCK_TYPE, TABLE_SCHEMA, TABLE_NAME FROM information_schema.metadata_lock_info -WHERE TABLE_NAME NOT LIKE 'innodb_%_stats'; +WHERE TABLE_NAME NOT LIKE 'innodb_%_stats' ORDER BY TABLE_NAME; LOCK_MODE LOCK_TYPE TABLE_SCHEMA TABLE_NAME MDL_BACKUP_WAIT_DDL Backup lock -MDL_SHARED_WRITE Table metadata lock test t1 MDL_INTENTION_EXCLUSIVE Schema metadata lock test +MDL_SHARED_WRITE Table metadata lock test t1 backup stage end; connection default; commit; @@ -143,7 +145,7 @@ DROP TABLE t1; connection con2; connection con2; SELECT LOCK_MODE, LOCK_TYPE, TABLE_SCHEMA, TABLE_NAME FROM information_schema.metadata_lock_info -WHERE TABLE_NAME NOT LIKE 'innodb_%_stats'; +WHERE TABLE_NAME NOT LIKE 'innodb_%_stats' ORDER BY TABLE_NAME; LOCK_MODE LOCK_TYPE TABLE_SCHEMA TABLE_NAME MDL_BACKUP_WAIT_DDL Backup lock SELECT * FROM t1; diff --git a/mysql-test/main/backup_lock.test b/mysql-test/main/backup_lock.test index eec6c2f01ce6b..14fa419c42009 100644 --- a/mysql-test/main/backup_lock.test +++ b/mysql-test/main/backup_lock.test @@ -13,7 +13,7 @@ --echo # let $mdl= LOCK_MODE, LOCK_TYPE, TABLE_SCHEMA, TABLE_NAME FROM information_schema.metadata_lock_info -WHERE TABLE_NAME NOT LIKE 'innodb_%_stats'; +WHERE TABLE_NAME NOT LIKE 'innodb_%_stats' ORDER BY TABLE_NAME; --source ../suite/innodb/include/wait_all_purged.inc @@ -62,6 +62,7 @@ let $wait_condition= where state = "Waiting for table metadata lock"; --source include/wait_condition.inc backup stage flush; +--replace_regex /#sql-alter-[0-9a-f_\-]*/#sql-alter/ eval SELECT $mdl; # # Do first test with max_statement_time, other tests later are done with @@ -111,6 +112,7 @@ let $wait_condition= --source include/wait_condition.inc backup stage start; backup stage flush; +--replace_regex /#sql-alter-[0-9a-f_\-]*/#sql-alter/ eval SELECT $mdl; backup stage block_ddl; backup stage block_commit; diff --git a/sql/ddl_log.cc b/sql/ddl_log.cc index db6801cf36c88..d1103e36ef087 100644 --- a/sql/ddl_log.cc +++ b/sql/ddl_log.cc @@ -85,7 +85,6 @@ uchar ddl_log_file_magic[]= { (uchar) 254, (uchar) 254, (uchar) 11, (uchar) 2 }; /* Action names for ddl_log_action_code */ - const char *ddl_log_action_name[DDL_LOG_LAST_ACTION]= { "Unknown", "partitioning delete", "partitioning rename", @@ -96,6 +95,136 @@ const char *ddl_log_action_name[DDL_LOG_LAST_ACTION]= "delete tmp file", "create trigger", "alter table", "store query" }; +/** + Acquire MDL exclusive lock on a single table name. + @param thd MySQL thread handle + @param db_name Database name + @param tbl_name Table name +*/ +static void ddl_log_acquire_table_mdl( + THD *thd, const char *db_name, const char *tbl_name) +{ + if (!db_name || !tbl_name || !*db_name || !*tbl_name) + return; + MDL_request request; + MDL_REQUEST_INIT(&request, MDL_key::TABLE, db_name, tbl_name, + MDL_EXCLUSIVE, MDL_TRANSACTION); + + thd->mdl_context.acquire_lock(&request, 0); +} + +/** + Conditionally acquire MDL based on database name availability. + If db_name is empty, treat table_name as full path and parse it. + Otherwise, use db_name and table_name directly. + @param thd MySQL thread handle + @param db_name Database name (may be empty) + @param table_name Table name or full path +*/ +static void ddl_log_acquire_table_mdl( + THD *thd, const LEX_CSTRING &db_name, const LEX_CSTRING &table_name) +{ + if (db_name.length == 0) + { + /* If db is empty, treat table_name as full path and parse it */ + const char *full_name= table_name.str; + if (!full_name || !*full_name) + return; + + /* Skip leading ./ if present */ + if (full_name[0] == '.' && full_name[1] == '/') + full_name+= 2; + + /* Find database/table separator (either '/' or '.') */ + const char *separator= strchr(full_name, '/'); + if (!separator) + separator= strchr(full_name, '.'); + + if (!separator) + return; + + const char *db_start= full_name; + const char *tbl_start= separator + 1; + size_t db_len= separator - db_start; + size_t tbl_len= strlen(tbl_start); + + if (db_len == 0 || tbl_len == 0 || db_len > NAME_LEN || tbl_len > NAME_LEN) + return; + + /* Handle partition table names - look for partition markers like #P# */ + const char *part_marker= strstr(tbl_start, "#P#"); + if (part_marker) + tbl_len= part_marker - tbl_start; + + /* Copy and null-terminate names */ + char parsed_db_name[NAME_LEN + 1]; + char parsed_tbl_name[NAME_LEN + 1]; + memcpy(parsed_db_name, db_start, db_len); + parsed_db_name[db_len]= '\0'; + memcpy(parsed_tbl_name, tbl_start, tbl_len); + parsed_tbl_name[tbl_len]= '\0'; + + ddl_log_acquire_table_mdl(thd, parsed_db_name, parsed_tbl_name); + } + else + { + /* Use db and table names directly */ + ddl_log_acquire_table_mdl(thd, db_name.str, table_name.str); + } +} + +/** + Acquire MDL exclusive locks for DDL log operations based on action type. + @param thd MySQL thread handle + @param ddl_log_entry DDL log entry containing action type and table names +*/ +static void ddl_log_acquire_mdl(THD *thd, DDL_LOG_ENTRY *ddl_log_entry) +{ + if (memcmp(ddl_log_entry->handler_name.str, "InnoDB", + ddl_log_entry->handler_name.length)) + return; + + switch (ddl_log_entry->action_type) { + case DDL_LOG_DELETE_ACTION: + case DDL_LOG_REPLACE_ACTION: + /* Acquire MDL on table being deleted/replaced */ + ddl_log_acquire_table_mdl(thd, ddl_log_entry->db, ddl_log_entry->name); + break; + + case DDL_LOG_RENAME_ACTION: + case DDL_LOG_RENAME_TABLE_ACTION: + /* Acquire MDL on both source and destination tables for rename */ + ddl_log_acquire_table_mdl(thd, ddl_log_entry->from_db, + ddl_log_entry->from_name); + ddl_log_acquire_table_mdl(thd, ddl_log_entry->db, ddl_log_entry->name); + break; + + case DDL_LOG_ALTER_TABLE_ACTION: + /* Acquire MDL on all tables involved in alter table operations */ + ddl_log_acquire_table_mdl(thd, ddl_log_entry->db, ddl_log_entry->name); + ddl_log_acquire_table_mdl(thd, ddl_log_entry->from_db, + ddl_log_entry->from_name); + ddl_log_acquire_table_mdl(thd, ddl_log_entry->db, ddl_log_entry->extra_name); + break; + + case DDL_LOG_EXCHANGE_ACTION: + { + /* Acquire MDL on primary table */ + ddl_log_acquire_table_mdl(thd, ddl_log_entry->db, ddl_log_entry->name); + + /* Acquire MDL on source table for exchange */ + ddl_log_acquire_table_mdl(thd, ddl_log_entry->from_db, + ddl_log_entry->from_name); + + ddl_log_acquire_table_mdl(thd, ddl_log_entry->db, ddl_log_entry->tmp_name); + break; + } + default: + /* No MDL acquisition needed for other action types */ + break; + } +} + /* Number of phases per entry */ const uchar ddl_log_entry_phases[DDL_LOG_LAST_ACTION]= { @@ -1446,6 +1575,10 @@ static int ddl_log_execute_action(THD *thd, MEM_ROOT *mem_root, invers_fn_flags|= FN_FROM_IS_TMP; } + if (!frm_action) + ddl_log_acquire_mdl(thd, ddl_log_entry); + + /* Acquire MDL for table operations */ switch (ddl_log_entry->action_type) { case DDL_LOG_REPLACE_ACTION: case DDL_LOG_DELETE_ACTION: diff --git a/sql/sql_partition_admin.cc b/sql/sql_partition_admin.cc index 6eb296d7762b8..b5ab3403feff4 100644 --- a/sql/sql_partition_admin.cc +++ b/sql/sql_partition_admin.cc @@ -661,6 +661,14 @@ bool Sql_cmd_alter_table_exchange_partition:: table_list->next_local->db.str, temp_name, "", FN_IS_TMP); + { + MDL_request mdl_request; + MDL_REQUEST_INIT(&mdl_request, MDL_key::TABLE, + table_list->next_local->db.str, + temp_name, MDL_EXCLUSIVE, MDL_TRANSACTION); + thd->mdl_context.acquire_lock(&mdl_request, 0); + } + if (unlikely(!(part_elem= part_table->part_info->get_part_elem(partition_name, part_file_name + diff --git a/sql/sql_table.cc b/sql/sql_table.cc index f6a2545c52279..466a049b6b4ce 100644 --- a/sql/sql_table.cc +++ b/sql/sql_table.cc @@ -11929,6 +11929,19 @@ do_continue:; */ alter_info->original_table= table; + { + /* + Create an exclusive lock on the temporary table name. + Needed by InnoDB storage engine to avoid MDL on source + table during purge. + */ + MDL_request mdl_request; + MDL_REQUEST_INIT(&mdl_request, MDL_key::TABLE, + alter_ctx.new_db.str, alter_ctx.tmp_name.str, + MDL_EXCLUSIVE, MDL_TRANSACTION); + thd->mdl_context.acquire_lock(&mdl_request, 0); + } + /* Create the .frm file for the new table. Storage engine table will not be created at this stage. @@ -12501,6 +12514,16 @@ do_continue:; If we are changing to use another table handler, we don't have to do the rename as the table names will not interfer. */ + /* + Create an exclusive lock on the backup table name. + Needed by InnoDB storage engine to avoid MDL conflicts + during backup table operations. + */ + MDL_request mdl_request; + MDL_REQUEST_INIT(&mdl_request, MDL_key::TABLE, + alter_ctx.db.str, backup_name.str, + MDL_EXCLUSIVE, MDL_TRANSACTION); + thd->mdl_context.acquire_lock(&mdl_request, 0); if (mysql_rename_table(old_db_type, &alter_ctx.db, &alter_ctx.table_name, &alter_ctx.db, &backup_name, &alter_ctx.id, FN_TO_IS_TMP | diff --git a/storage/innobase/dict/dict0dict.cc b/storage/innobase/dict/dict0dict.cc index 2edc7068f17e3..1342171a441a8 100644 --- a/storage/innobase/dict/dict0dict.cc +++ b/storage/innobase/dict/dict0dict.cc @@ -519,59 +519,105 @@ void mdl_release(THD *thd, MDL_ticket *mdl) noexcept thd->mdl_context.release_lock(mdl); } +void dict_table_t::parse_tbl_name( + const table_name_t &table_name, + char (&db_name)[NAME_LEN + 1], + char (&tbl_name)[NAME_LEN + 1], + size_t *db_name_len, size_t *tbl_name_len) noexcept +{ + char db_buf[MAX_DATABASE_NAME_LEN + 1]; + char tbl_buf[MAX_TABLE_NAME_LEN + 1]; + + const size_t db_len= table_name.dblen(); + const char* tbl_start= table_name.basename(); + + if (db_len == 0 || !tbl_start) + return; + + memcpy(db_buf, table_name.m_name, db_len); + db_buf[db_len]= '\0'; + + size_t tbl_len= strlen(tbl_start); + const bool is_temp= table_name.is_temporary() || table_name.is_create_or_replace(); + + /* For partition tables, find the base table name by removing partition suffix */ + if (const char *is_part = static_cast + (memchr(tbl_start, '#', tbl_len))) + { + /* For temporary tables, we need to find the partition marker after the temp name */ + if (is_temp) + { + /* Look for partition markers like #P# after the temporary table name */ + const char *part_marker= strstr(tbl_start, "#P#"); + if (part_marker) + tbl_len= static_cast(part_marker - tbl_start); + else if (const char *vec_marker= strstr(tbl_start, "#i#")) + tbl_len= static_cast(vec_marker - tbl_start); + } + else + { + /* For regular tables, use first # as partition boundary */ + tbl_len= static_cast(is_part - tbl_start); + } + } + + memcpy(tbl_buf, tbl_start, tbl_len); + tbl_buf[tbl_len]= '\0'; + + /* Convert internal names to SQL names */ + *db_name_len= filename_to_tablename(db_buf, db_name, + MAX_DATABASE_NAME_LEN + 1, true); + if (is_temp) + { + memcpy(tbl_name, tbl_buf, tbl_len); + tbl_name[tbl_len]= '\0'; + *tbl_name_len= tbl_len; + return; + } + *tbl_name_len= filename_to_tablename(tbl_buf, tbl_name, + MAX_TABLE_NAME_LEN + 1, true); + return; +} + /** Parse the table file name into table name and database name. @tparam dict_frozen whether the caller holds dict_sys.latch @param[in,out] db_name database name buffer @param[in,out] tbl_name table name buffer @param[out] db_name_len database name length @param[out] tbl_name_len table name length -@return whether the table name is visible to SQL */ +@return DB_CORRUPTION if table name contains #sql-ib, + DB_SUCCESS_LOCKED_REC if db_len == 0, DB_SUCCESS otherwise */ template -bool dict_table_t::parse_name(char (&db_name)[NAME_LEN + 1], - char (&tbl_name)[NAME_LEN + 1], - size_t *db_name_len, size_t *tbl_name_len) const +dberr_t dict_table_t::parse_name(char (&db_name)[NAME_LEN + 1], + char (&tbl_name)[NAME_LEN + 1], + size_t *db_name_len, size_t *tbl_name_len) const { - char db_buf[MAX_DATABASE_NAME_LEN + 1]; - char tbl_buf[MAX_TABLE_NAME_LEN + 1]; - if (!dict_frozen) dict_sys.freeze(SRW_LOCK_CALL); /* protect against renaming */ ut_ad(dict_sys.frozen()); - const size_t db_len= name.dblen(); - ut_ad(db_len <= MAX_DATABASE_NAME_LEN); - - memcpy(db_buf, mdl_name.m_name, db_len); - db_buf[db_len]= 0; - - size_t tbl_len= strlen(mdl_name.m_name + db_len + 1); - const bool is_temp= mdl_name.is_temporary(); - - if (is_temp); - else if (const char *is_part= static_cast - (memchr(mdl_name.m_name + db_len + 1, '#', tbl_len))) - tbl_len= static_cast(is_part - &mdl_name.m_name[db_len + 1]); - - memcpy(tbl_buf, mdl_name.m_name + db_len + 1, tbl_len); - tbl_buf[tbl_len]= 0; - + parse_tbl_name(name, db_name, tbl_name, db_name_len, tbl_name_len); if (!dict_frozen) dict_sys.unfreeze(); - *db_name_len= filename_to_tablename(db_buf, db_name, - MAX_DATABASE_NAME_LEN + 1, true); + /* Check for specific error conditions */ + if (*db_name_len == 0) + return DB_SUCCESS_LOCKED_REC; - if (is_temp) - return false; + /* Check if table name contains #sql-ib */ + if (strstr(tbl_name, "#sql-ib")) + return DB_CORRUPTION; - *tbl_name_len= filename_to_tablename(tbl_buf, tbl_name, - MAX_TABLE_NAME_LEN + 1, true); - return true; + return DB_SUCCESS; } -template bool +template dberr_t dict_table_t::parse_name<>(char(&)[NAME_LEN + 1], char(&)[NAME_LEN + 1], size_t*, size_t*) const; +template dberr_t +dict_table_t::parse_name(char(&)[NAME_LEN + 1], char(&)[NAME_LEN + 1], + size_t*, size_t*) const; + dict_table_t *dict_sys_t::acquire_temporary_table(table_id_t id) const noexcept { ut_ad(frozen()); @@ -631,10 +677,12 @@ dict_acquire_mdl_shared(dict_table_t *table, char tbl_buf[NAME_LEN + 1], tbl_buf1[NAME_LEN + 1]; size_t db_len, tbl_len; - if (!table->parse_name(db_buf, tbl_buf, &db_len, &tbl_len)) - /* The name of an intermediate table starts with #sql */ + dberr_t err= table->parse_name( + db_buf, tbl_buf, &db_len, &tbl_len); + if (err == DB_SUCCESS_LOCKED_REC) return table; + ut_a(err == DB_SUCCESS); retry: ut_ad(!trylock == dict_sys.frozen()); @@ -701,12 +749,16 @@ dict_acquire_mdl_shared(dict_table_t *table, if (trylock) table->acquire(); - if (!table->parse_name(db_buf1, tbl_buf1, &db1_len, &tbl1_len)) + dberr_t err= + table->parse_name(db_buf1, tbl_buf1, &db1_len, &tbl1_len); + if (err == DB_SUCCESS_LOCKED_REC) { /* The table was renamed to #sql prefix. Release MDL (if any) for the old name and return. */ goto unlock_and_return_without_mdl; } + + ut_a(err == DB_SUCCESS); } else if (table_op != DICT_TABLE_OP_OPEN_ONLY_IF_CACHED) { diff --git a/storage/innobase/handler/ha_innodb.cc b/storage/innobase/handler/ha_innodb.cc index e93fabb0291f8..f01ed106a09bd 100644 --- a/storage/innobase/handler/ha_innodb.cc +++ b/storage/innobase/handler/ha_innodb.cc @@ -117,6 +117,7 @@ this program; if not, write to the Free Software Foundation, Inc., #include #include // TT_FOR_UPGRADE #include "sql_type_vector.h" +#include #define thd_get_query_id(thd) uint64_t((thd)->query_id) #define thd_in_lock_tables(thd) (thd)->in_lock_tables @@ -225,6 +226,79 @@ static my_bool innodb_read_only_compressed; /** A dummy variable */ static uint innodb_max_purge_lag_wait; +/** Print MDL acquisition status for a table name. +@param thd MySQL thread handle +@param table_name table name object +@param op operation description for logging */ +void innodb_print_mdl_status( + THD *thd, const table_name_t& table_name, const char* op) +{ + char db_name[NAME_LEN + 1]; + char tbl_name[NAME_LEN + 1]; + char sql_db_name[NAME_LEN + 1]; + char sql_tbl_name[NAME_LEN + 1]; + /* Extract database and table names using table_name_t methods */ + size_t db_len = table_name.dblen(); + const char* tbl_start = table_name.basename(); + + if (db_len > 0 && tbl_start) + { + /* Copy database name */ + memcpy(db_name, table_name.m_name, db_len); + db_name[db_len] = '\0'; + + /* Copy table name */ + strncpy(tbl_name, tbl_start, NAME_LEN); + tbl_name[NAME_LEN] = '\0'; + + /* Convert internal names to SQL names using filename_to_tablename */ + filename_to_tablename(db_name, sql_db_name, sizeof(sql_db_name)); + filename_to_tablename(tbl_name, sql_tbl_name, sizeof(sql_tbl_name)); + + MDL_context *mdl_context= + static_cast(thd_mdl_context(thd)); + + bool is_locked= false; + if (mdl_context->is_lock_owner(MDL_key::TABLE, sql_db_name, + sql_tbl_name, MDL_EXCLUSIVE)) + is_locked= true; + + ib::info() << "MDL Status [" << op << "]: " + << "Database: '" << sql_db_name << "' " + << "Table: '" << sql_tbl_name << "' " + << "MDL lock: " << (is_locked ? "YES" : "NO"); + } + else + ib::info() << "MDL Status [" << op << "]: " + << "Invalid table name format: '" << table_name.m_name << "'"; +} + +/** Assert that current thread holds MDL on a table name. +This function is intended for debugging rename_table() and drop_table() +operations to ensure proper MDL acquisition. +@param thd MySQL thread handle +@param table_name table name to check MDL ownership on */ +static bool innodb_has_mdl_check(THD *thd, const char *table_name) +{ + char db_buf[NAME_LEN + 1]; + char tbl_buf[NAME_LEN + 1]; + size_t tbl_len, db_len; + + dict_table_t::parse_tbl_name( + table_name_t(const_cast(table_name)), + db_buf, tbl_buf, &db_len, &tbl_len); + + if (strstr(tbl_buf, "sql-ib")) + return true; + + MDL_key key; + key.mdl_key_init(MDL_key::TABLE, db_buf, tbl_buf); + if (thd->mdl_context.is_lock_owner(MDL_key::TABLE, db_buf, tbl_buf, + MDL_EXCLUSIVE)) + return true; + return false; +} + /** Wait for trx_sys.history_size() to be below a limit. */ static void innodb_max_purge_lag_wait_update(THD *thd, st_mysql_sys_var *, void *, const void *limit) @@ -13727,6 +13801,7 @@ int ha_innobase::delete_table(const char *name) if (err != DB_SUCCESS) goto err_exit; + ut_ad(innodb_has_mdl_check(trx->mysql_thd, table->name.m_name)); err= trx->drop_table(*table); if (err != DB_SUCCESS) goto err_exit; @@ -13775,6 +13850,8 @@ static dberr_t innobase_rename_table(trx_t *trx, const char *from, ut_ad(trx->will_lock); + ut_ad(innodb_has_mdl_check(trx->mysql_thd, norm_from)); + ut_ad(innodb_has_mdl_check(trx->mysql_thd, norm_to)); error = row_rename_table_for_mysql(norm_from, norm_to, trx, fk); if (error != DB_SUCCESS) { @@ -20384,9 +20461,12 @@ static TABLE* innodb_find_table_for_vc(THD* thd, dict_table_t* table) char tbl_buf[NAME_LEN + 1]; ulint db_buf_len, tbl_buf_len; - if (!table->parse_name(db_buf, tbl_buf, &db_buf_len, &tbl_buf_len)) { + dberr_t err= table->parse_name( + db_buf, tbl_buf, &db_buf_len, &tbl_buf_len); + if (err == DB_SUCCESS_LOCKED_REC) { return NULL; } + ut_a(err == DB_SUCCESS); if (bg_thread) { return open_purge_table(thd, db_buf, db_buf_len, diff --git a/storage/innobase/include/dict0mem.h b/storage/innobase/include/dict0mem.h index f004594269bb0..4a8e80f9b2da1 100644 --- a/storage/innobase/include/dict0mem.h +++ b/storage/innobase/include/dict0mem.h @@ -2014,6 +2014,11 @@ struct dict_table_t { return strstr(name, "/#sql-create-"); } + static bool is_inplace_temp_name(const char* name) noexcept + { + return strstr(name, "#sql-ib"); + } + /** @return whether instant ALTER TABLE is in effect */ bool is_instant() const noexcept { @@ -2117,17 +2122,24 @@ struct dict_table_t { /** For overflow fields returns potential max length stored inline */ inline size_t get_overflow_field_local_len() const; + static void parse_tbl_name( + const table_name_t &table_name, + char (&db_name)[NAME_LEN + 1], + char (&tbl_name)[NAME_LEN + 1], + size_t *db_name_len, size_t *tbl_name_len) noexcept; + /** Parse the table file name into table name and database name. @tparam dict_frozen whether the caller holds dict_sys.latch @param[in,out] db_name database name buffer @param[in,out] tbl_name table name buffer @param[out] db_name_len database name length @param[out] tbl_name_len table name length - @return whether the table name is visible to SQL */ + @return DB_SUCCESS_LOCKED_REC if table name contains #sql-ib, + DB_CORRUPTION if db_len == 0, DB_SUCCESS otherwise */ template - bool parse_name(char (&db_name)[NAME_LEN + 1], - char (&tbl_name)[NAME_LEN + 1], - size_t *db_name_len, size_t *tbl_name_len) const; + dberr_t parse_name(char (&db_name)[NAME_LEN + 1], + char (&tbl_name)[NAME_LEN + 1], + size_t *db_name_len, size_t *tbl_name_len) const; /** Clear the table when rolling back TRX_UNDO_EMPTY @return error code */ diff --git a/storage/innobase/include/ha_prototypes.h b/storage/innobase/include/ha_prototypes.h index 08ef2050b48ab..d806f4558f403 100644 --- a/storage/innobase/include/ha_prototypes.h +++ b/storage/innobase/include/ha_prototypes.h @@ -402,6 +402,13 @@ allocated from heap */ char *dict_table_lookup(LEX_CSTRING db, LEX_CSTRING name, dict_table_t **table, mem_heap_t *heap) noexcept; +/** Print MDL acquisition status for a table name. +@param thd MySQL thread handle +@param table_name table name object +@param op operation description for logging */ +void innodb_print_mdl_status( + THD *thd, const table_name_t& table_name, const char* op); + #ifdef WITH_WSREP /** Append table-level exclusive key. @param thd MySQL thread handle diff --git a/storage/innobase/row/row0mysql.cc b/storage/innobase/row/row0mysql.cc index 6098d098c6478..f4b36600ebb93 100644 --- a/storage/innobase/row/row0mysql.cc +++ b/storage/innobase/row/row0mysql.cc @@ -2620,6 +2620,11 @@ row_rename_table_for_mysql( goto funct_exit; } } + //innodb_print_mdl_status(trx->mysql_thd, table->name, + // "RENAME TABLE - FROM"); + //innodb_print_mdl_status( + // trx->mysql_thd, table_name_t(const_cast(new_name)), + // "RENAME TABLE - TO"); err = trx_undo_report_rename(trx, table); diff --git a/storage/innobase/trx/trx0purge.cc b/storage/innobase/trx/trx0purge.cc index 9ba4a8f3e190f..3ff18a0c3441b 100644 --- a/storage/innobase/trx/trx0purge.cc +++ b/storage/innobase/trx/trx0purge.cc @@ -1103,9 +1103,12 @@ static dict_table_t *trx_purge_table_acquire(dict_table_t *table, char tbl_buf[NAME_LEN + 1]; size_t tbl_len; - if (!table->parse_name(db_buf, tbl_buf, &db_len, &tbl_len)) - /* The name of an intermediate table starts with #sql */ + dberr_t parse_result = table->parse_name( + db_buf, tbl_buf, &db_len, &tbl_len); + if (parse_result == DB_SUCCESS_LOCKED_REC) + /* The name of an intermediate table starts with #sql-ib */ goto got_table; + ut_a(parse_result == DB_SUCCESS); { MDL_request request;