diff --git a/mysql-test/r/mdl_sync.result b/mysql-test/r/mdl_sync.result index 36451985a86..f63179b893a 100644 --- a/mysql-test/r/mdl_sync.result +++ b/mysql-test/r/mdl_sync.result @@ -62,3 +62,114 @@ unlock tables; # Reap INSERT. # Clean-up. drop tables t1, t2, t3, t5; +# +# Bug #46044 "MDL deadlock on LOCK TABLE + CREATE TABLE HIGH_PRIORITY +# FOR UPDATE" +# +drop tables if exists t1, t2; +create table t1 (i int); +# Let us check that we won't deadlock if during filling +# of I_S table we encounter conflicting metadata lock +# which owner is in its turn waiting for our connection. +lock tables t1 write; +# Switching to connection 'con46044'. +# Sending: +create table t2 select * from t1;; +# Switching to connection 'default'. +# Waiting until CREATE TABLE ... SELECT ... is blocked. +# First let us check that SHOW FIELDS/DESCRIBE doesn't +# gets blocked and emits and error. +show fields from t2; +ERROR HY000: Table 'test'.'t2' was skipped since its definition is being modified by concurrent DDL statement +# Now test for I_S query which reads only .FRMs. +# +# Query below should only emit a warning. +select column_name from information_schema.columns +where table_schema='test' and table_name='t2'; +column_name +Warnings: +Warning 1652 Table 'test'.'t2' was skipped since its definition is being modified by concurrent DDL statement +# Finally, test for I_S query which does full-blown table open. +# +# Query below should not be blocked. Warning message should be +# stored in the 'table_comment' column. +select table_name, table_type, auto_increment, table_comment +from information_schema.tables where table_schema='test' and table_name='t2'; +table_name table_type auto_increment table_comment +t2 BASE TABLE NULL Table 'test'.'t2' was skipped since its definition is being modified by concurre +# Switching to connection 'default'. +unlock tables; +# Switching to connection 'con46044'. +# Reaping CREATE TABLE ... SELECT ... . +drop table t2; +# +# Let us also check that queries to I_S wait for conflicting metadata +# locks to go away instead of skipping table with a warning in cases +# when deadlock is not possible. This is a nice thing from compatibility +# and ease of use points of view. +# +# We check same three queries to I_S in this new situation. +# Switching to connection 'con46044_2'. +lock tables t1 write; +# Switching to connection 'con46044'. +# Sending: +create table t2 select * from t1;; +# Switching to connection 'default'. +# Waiting until CREATE TABLE ... SELECT ... is blocked. +# Let us check that SHOW FIELDS/DESCRIBE gets blocked. +# Sending: +show fields from t2;; +# Switching to connection 'con46044_2'. +# Wait until SHOW FIELDS gets blocked. +unlock tables; +# Switching to connection 'con46044'. +# Reaping CREATE TABLE ... SELECT ... . +# Switching to connection 'default'. +# Reaping SHOW FIELDS ... +Field Type Null Key Default Extra +i int(11) YES NULL +drop table t2; +# Switching to connection 'con46044_2'. +lock tables t1 write; +# Switching to connection 'con46044'. +# Sending: +create table t2 select * from t1;; +# Switching to connection 'default'. +# Waiting until CREATE TABLE ... SELECT ... is blocked. +# Check that I_S query which reads only .FRMs gets blocked. +# Sending: +select column_name from information_schema.columns where table_schema='test' and table_name='t2';; +# Switching to connection 'con46044_2'. +# Wait until SELECT COLUMN_NAME FROM I_S.COLUMNS gets blocked. +unlock tables; +# Switching to connection 'con46044'. +# Reaping CREATE TABLE ... SELECT ... . +# Switching to connection 'default'. +# Reaping SELECT COLUMN_NAME FROM I_S.COLUMNS +column_name +i +drop table t2; +# Switching to connection 'con46044_2'. +lock tables t1 write; +# Switching to connection 'con46044'. +# Sending: +create table t2 select * from t1;; +# Switching to connection 'default'. +# Waiting until CREATE TABLE ... SELECT ... is blocked. +# Finally, check that I_S query which does full-blown table open +# also gets blocked. +# Sending: +select table_name, table_type, auto_increment, table_comment from information_schema.tables where table_schema='test' and table_name='t2';; +# Switching to connection 'con46044_2'. +# Wait until SELECT ... FROM I_S.TABLES gets blocked. +unlock tables; +# Switching to connection 'con46044'. +# Reaping CREATE TABLE ... SELECT ... . +# Switching to connection 'default'. +# Reaping SELECT ... FROM I_S.TABLES +table_name table_type auto_increment table_comment +t2 BASE TABLE NULL +drop table t2; +# Switching to connection 'default'. +# Clean-up. +drop table t1; diff --git a/mysql-test/t/mdl_sync.test b/mysql-test/t/mdl_sync.test index 124e76bbc1f..cc03cc67f95 100644 --- a/mysql-test/t/mdl_sync.test +++ b/mysql-test/t/mdl_sync.test @@ -141,6 +141,208 @@ disconnect con2root; drop tables t1, t2, t3, t5; +--echo # +--echo # Bug #46044 "MDL deadlock on LOCK TABLE + CREATE TABLE HIGH_PRIORITY +--echo # FOR UPDATE" +--echo # +--disable_warnings +drop tables if exists t1, t2; +--enable_warnings +connect (con46044, localhost, root,,); +connect (con46044_2, localhost, root,,); +connection default; +create table t1 (i int); + +--echo # Let us check that we won't deadlock if during filling +--echo # of I_S table we encounter conflicting metadata lock +--echo # which owner is in its turn waiting for our connection. +lock tables t1 write; + +--echo # Switching to connection 'con46044'. +connection con46044; +--echo # Sending: +--send create table t2 select * from t1; + +--echo # Switching to connection 'default'. +connection default; +--echo # Waiting until CREATE TABLE ... SELECT ... is blocked. +let $wait_condition= + select count(*) = 1 from information_schema.processlist + where state = "Table lock" and info = "create table t2 select * from t1"; +--source include/wait_condition.inc + +--echo # First let us check that SHOW FIELDS/DESCRIBE doesn't +--echo # gets blocked and emits and error. +--error ER_WARN_I_S_SKIPPED_TABLE +show fields from t2; + +--echo # Now test for I_S query which reads only .FRMs. +--echo # +--echo # Query below should only emit a warning. +select column_name from information_schema.columns + where table_schema='test' and table_name='t2'; + +--echo # Finally, test for I_S query which does full-blown table open. +--echo # +--echo # Query below should not be blocked. Warning message should be +--echo # stored in the 'table_comment' column. +select table_name, table_type, auto_increment, table_comment + from information_schema.tables where table_schema='test' and table_name='t2'; + +--echo # Switching to connection 'default'. +connection default; +unlock tables; + +--echo # Switching to connection 'con46044'. +connection con46044; +--echo # Reaping CREATE TABLE ... SELECT ... . +--reap +drop table t2; + +--echo # +--echo # Let us also check that queries to I_S wait for conflicting metadata +--echo # locks to go away instead of skipping table with a warning in cases +--echo # when deadlock is not possible. This is a nice thing from compatibility +--echo # and ease of use points of view. +--echo # +--echo # We check same three queries to I_S in this new situation. + +--echo # Switching to connection 'con46044_2'. +connection con46044_2; +lock tables t1 write; + +--echo # Switching to connection 'con46044'. +connection con46044; +--echo # Sending: +--send create table t2 select * from t1; + +--echo # Switching to connection 'default'. +connection default; +--echo # Waiting until CREATE TABLE ... SELECT ... is blocked. +let $wait_condition= + select count(*) = 1 from information_schema.processlist + where state = "Table lock" and info = "create table t2 select * from t1"; +--source include/wait_condition.inc + +--echo # Let us check that SHOW FIELDS/DESCRIBE gets blocked. +--echo # Sending: +--send show fields from t2; + +--echo # Switching to connection 'con46044_2'. +connection con46044_2; +--echo # Wait until SHOW FIELDS gets blocked. +let $wait_condition= + select count(*) = 1 from information_schema.processlist + where state = "Waiting for table" and info = "show fields from t2"; +--source include/wait_condition.inc + +unlock tables; + +--echo # Switching to connection 'con46044'. +connection con46044; +--echo # Reaping CREATE TABLE ... SELECT ... . +--reap + +--echo # Switching to connection 'default'. +connection default; +--echo # Reaping SHOW FIELDS ... +--reap +drop table t2; + +--echo # Switching to connection 'con46044_2'. +connection con46044_2; +lock tables t1 write; + +--echo # Switching to connection 'con46044'. +connection con46044; +--echo # Sending: +--send create table t2 select * from t1; + +--echo # Switching to connection 'default'. +connection default; +--echo # Waiting until CREATE TABLE ... SELECT ... is blocked. +let $wait_condition= + select count(*) = 1 from information_schema.processlist + where state = "Table lock" and info = "create table t2 select * from t1"; +--source include/wait_condition.inc + +--echo # Check that I_S query which reads only .FRMs gets blocked. +--echo # Sending: +--send select column_name from information_schema.columns where table_schema='test' and table_name='t2'; + +--echo # Switching to connection 'con46044_2'. +connection con46044_2; +--echo # Wait until SELECT COLUMN_NAME FROM I_S.COLUMNS gets blocked. +let $wait_condition= + select count(*) = 1 from information_schema.processlist + where state = "Waiting for table" and + info like "select column_name from information_schema.columns%"; +--source include/wait_condition.inc + +unlock tables; + +--echo # Switching to connection 'con46044'. +connection con46044; +--echo # Reaping CREATE TABLE ... SELECT ... . +--reap + +--echo # Switching to connection 'default'. +connection default; +--echo # Reaping SELECT COLUMN_NAME FROM I_S.COLUMNS +--reap +drop table t2; + +--echo # Switching to connection 'con46044_2'. +connection con46044_2; +lock tables t1 write; + +--echo # Switching to connection 'con46044'. +connection con46044; +--echo # Sending: +--send create table t2 select * from t1; + +--echo # Switching to connection 'default'. +connection default; +--echo # Waiting until CREATE TABLE ... SELECT ... is blocked. +let $wait_condition= + select count(*) = 1 from information_schema.processlist + where state = "Table lock" and info = "create table t2 select * from t1"; +--source include/wait_condition.inc + +--echo # Finally, check that I_S query which does full-blown table open +--echo # also gets blocked. +--echo # Sending: +--send select table_name, table_type, auto_increment, table_comment from information_schema.tables where table_schema='test' and table_name='t2'; + +--echo # Switching to connection 'con46044_2'. +connection con46044_2; +--echo # Wait until SELECT ... FROM I_S.TABLES gets blocked. +let $wait_condition= + select count(*) = 1 from information_schema.processlist + where state = "Waiting for table" and + info like "select table_name, table_type, auto_increment, table_comment from information_schema.tables%"; +--source include/wait_condition.inc + +unlock tables; + +--echo # Switching to connection 'con46044'. +connection con46044; +--echo # Reaping CREATE TABLE ... SELECT ... . +--reap + +--echo # Switching to connection 'default'. +connection default; +--echo # Reaping SELECT ... FROM I_S.TABLES +--reap +drop table t2; + +--echo # Switching to connection 'default'. +connection default; +--echo # Clean-up. +disconnect con46044; +disconnect con46044_2; +drop table t1; + # Check that all connections opened by test cases in this file are really # gone so execution of other tests won't be affected by their presence. --source include/wait_until_count_sessions.inc diff --git a/sql/mysql_priv.h b/sql/mysql_priv.h index e61c9a923c3..03a4462f199 100644 --- a/sql/mysql_priv.h +++ b/sql/mysql_priv.h @@ -2071,6 +2071,8 @@ MYSQL_LOCK *mysql_lock_tables(THD *thd, TABLE **table, uint count, #define MYSQL_OPEN_GET_NEW_TABLE 0x0080 /** Don't look up the table in the list of temporary tables. */ #define MYSQL_OPEN_SKIP_TEMPORARY 0x0100 +/** Fail instead of waiting when conficting metadata lock is discovered. */ +#define MYSQL_OPEN_FAIL_ON_MDL_CONFLICT 0x0200 /** Please refer to the internals manual. */ #define MYSQL_OPEN_REOPEN (MYSQL_LOCK_IGNORE_FLUSH |\ diff --git a/sql/share/errmsg-utf8.txt b/sql/share/errmsg-utf8.txt index 4260efdeb56..70fae60d051 100644 --- a/sql/share/errmsg-utf8.txt +++ b/sql/share/errmsg-utf8.txt @@ -6236,12 +6236,13 @@ ER_COND_ITEM_TOO_LONG eng "Data too long for condition item '%s'" ER_UNKNOWN_LOCALE - eng "Unknown locale: '%-.64s'" - + eng "Unknown locale: '%-.64s'" ER_SLAVE_IGNORE_SERVER_IDS eng "The requested server id %d clashes with the slave startup option --replicate-same-server-id" ER_QUERY_CACHE_DISABLED eng "Query cache is disabled; restart the server with query_cache_type=1 to enable it" +ER_WARN_I_S_SKIPPED_TABLE + eng "Table '%s'.'%s' was skipped since its definition is being modified by concurrent DDL statement" ER_SAME_NAME_PARTITION_FIELD eng "Duplicate partition field name '%-.192s'" ER_PARTITION_COLUMN_LIST_ERROR diff --git a/sql/share/errmsg.txt b/sql/share/errmsg.txt index 93385292c24..0f8a43407fa 100644 --- a/sql/share/errmsg.txt +++ b/sql/share/errmsg.txt @@ -6242,6 +6242,8 @@ ER_UNKNOWN_LOCALE ER_SLAVE_IGNORE_SERVER_IDS eng "The requested server id %d clashes with the slave startup option --replicate-same-server-id" +ER_WARN_I_S_SKIPPED_TABLE + eng "Table '%s'.'%s' was skipped since its definition is being modified by concurrent DDL statement" ER_SAME_NAME_PARTITION_FIELD eng "Duplicate partition field name '%-.192s'" ER_PARTITION_COLUMN_LIST_ERROR diff --git a/sql/sql_base.cc b/sql/sql_base.cc index f9514a055ca..d1ce4aefb1f 100644 --- a/sql/sql_base.cc +++ b/sql/sql_base.cc @@ -2350,7 +2350,10 @@ open_table_get_mdl_lock(THD *thd, TABLE_LIST *table_list, return 1; if (mdl_request->ticket == NULL) { - (void) ot_ctx->request_backoff_action(Open_table_context::OT_WAIT); + if (flags & MYSQL_OPEN_FAIL_ON_MDL_CONFLICT) + my_error(ER_WARN_I_S_SKIPPED_TABLE, MYF(0), table_list->db, table_list->table_name); + else + (void) ot_ctx->request_backoff_action(Open_table_context::OT_WAIT); return 1; } } diff --git a/sql/sql_show.cc b/sql/sql_show.cc index d346bae5258..fe941321dcd 100644 --- a/sql/sql_show.cc +++ b/sql/sql_show.cc @@ -2867,6 +2867,10 @@ make_table_name_list(THD *thd, List *table_names, LEX *lex, @param[in] thd thread handler @param[in] tables TABLE_LIST for I_S table @param[in] schema_table pointer to I_S structure + @param[in] can_deadlock Indicates that deadlocks are possible + due to metadata locks, so to avoid + them we should not wait in case if + conflicting lock is present. @param[in] open_tables_state_backup pointer to Open_tables_state object which is used to save|restore original status of variables related to @@ -2880,6 +2884,7 @@ make_table_name_list(THD *thd, List *table_names, LEX *lex, static int fill_schema_show_cols_or_idxs(THD *thd, TABLE_LIST *tables, ST_SCHEMA_TABLE *schema_table, + bool can_deadlock, Open_tables_state *open_tables_state_backup) { LEX *lex= thd->lex; @@ -2908,7 +2913,9 @@ fill_schema_show_cols_or_idxs(THD *thd, TABLE_LIST *tables, */ lex->sql_command= SQLCOM_SHOW_FIELDS; res= open_normal_and_derived_tables(thd, show_table_list, - MYSQL_LOCK_IGNORE_FLUSH); + (MYSQL_LOCK_IGNORE_FLUSH | + (can_deadlock ? + MYSQL_OPEN_FAIL_ON_MDL_CONFLICT : 0))); lex->sql_command= save_sql_command; /* get_all_tables() returns 1 on failure and 0 on success thus @@ -3047,12 +3054,16 @@ uint get_table_open_method(TABLE_LIST *tables, /** - Acquire high priority share metadata lock on a table. + Try acquire high priority share metadata lock on a table (with + optional wait for conflicting locks to go away). @param thd Thread context. @param mdl_request Pointer to memory to be used for MDL_request object for a lock request. @param table Table list element for the table + @param can_deadlock Indicates that deadlocks are possible due to + metadata locks, so to avoid them we should not + wait in case if conflicting lock is present. @note This is an auxiliary function to be used in cases when we want to access table's description by looking up info in TABLE_SHARE without @@ -3060,19 +3071,21 @@ uint get_table_open_method(TABLE_LIST *tables, @note This function assumes that there are no other metadata lock requests in the current metadata locking context. - @retval FALSE Success + @retval FALSE No error, if lock was obtained TABLE_LIST::mdl_request::ticket + is set to non-NULL value. @retval TRUE Some error occured (probably thread was killed). */ static bool -acquire_high_prio_shared_mdl_lock(THD *thd, TABLE_LIST *table) +try_acquire_high_prio_shared_mdl_lock(THD *thd, TABLE_LIST *table, + bool can_deadlock) { bool error; table->mdl_request.init(MDL_TABLE, table->db, table->table_name, MDL_SHARED_HIGH_PRIO); while (!(error= thd->mdl_context.try_acquire_shared_lock(&table->mdl_request)) && - !table->mdl_request.ticket) + !table->mdl_request.ticket && !can_deadlock) { MDL_request_list mdl_requests; mdl_requests.push_front(&table->mdl_request); @@ -3092,6 +3105,10 @@ acquire_high_prio_shared_mdl_lock(THD *thd, TABLE_LIST *table) @param[in] db_name database name @param[in] table_name table name @param[in] schema_table_idx I_S table index + @param[in] can_deadlock Indicates that deadlocks are possible + due to metadata locks, so to avoid + them we should not wait in case if + conflicting lock is present. @return Operation status @retval 0 Table is processed and we can continue @@ -3104,13 +3121,14 @@ static int fill_schema_table_from_frm(THD *thd,TABLE *table, ST_SCHEMA_TABLE *schema_table, LEX_STRING *db_name, LEX_STRING *table_name, - enum enum_schema_tables schema_table_idx) + enum enum_schema_tables schema_table_idx, + bool can_deadlock) { TABLE_SHARE *share; TABLE tbl; TABLE_LIST table_list; uint res= 0; - int error; + int not_used; char key[MAX_DBKEY_LENGTH]; uint key_length; char db_name_buff[NAME_LEN + 1], table_name_buff[NAME_LEN + 1]; @@ -3143,7 +3161,7 @@ static int fill_schema_table_from_frm(THD *thd,TABLE *table, simply obtaining internal lock of data-dictionary (ATM it is LOCK_open) instead of obtaning full-blown metadata lock. */ - if (acquire_high_prio_shared_mdl_lock(thd, &table_list)) + if (try_acquire_high_prio_shared_mdl_lock(thd, &table_list, can_deadlock)) { /* Some error occured (most probably we have been killed while @@ -3153,14 +3171,30 @@ static int fill_schema_table_from_frm(THD *thd,TABLE *table, return 1; } + if (! table_list.mdl_request.ticket) + { + /* + We are in situation when we have encountered conflicting metadata + lock and deadlocks can occur due to waiting for it to go away. + So instead of waiting skip this table with an appropriate warning. + */ + DBUG_ASSERT(can_deadlock); + + push_warning_printf(thd, MYSQL_ERROR::WARN_LEVEL_WARN, + ER_WARN_I_S_SKIPPED_TABLE, + ER(ER_WARN_I_S_SKIPPED_TABLE), + table_list.db, table_list.table_name); + return 0; + } + key_length= create_table_def_key(thd, key, &table_list, 0); pthread_mutex_lock(&LOCK_open); share= get_table_share(thd, &table_list, key, - key_length, OPEN_VIEW, &error); + key_length, OPEN_VIEW, ¬_used); if (!share) { res= 0; - goto err_unlock; + goto end_unlock; } if (share->is_view) @@ -3169,7 +3203,7 @@ static int fill_schema_table_from_frm(THD *thd,TABLE *table, { /* skip view processing */ res= 0; - goto err_share; + goto end_share; } else if (schema_table->i_s_requested_object & OPEN_VIEW_FULL) { @@ -3178,7 +3212,7 @@ static int fill_schema_table_from_frm(THD *thd,TABLE *table, open_normal_and_derived_tables() */ res= 1; - goto err_share; + goto end_share; } } @@ -3194,15 +3228,16 @@ static int fill_schema_table_from_frm(THD *thd,TABLE *table, res= schema_table->process_table(thd, &table_list, table, res, db_name, table_name); closefrm(&tbl, true); - goto err_unlock; + goto end_unlock; } -err_share: +end_share: release_table_share(share); -err_unlock: +end_unlock: pthread_mutex_unlock(&LOCK_open); +end: thd->mdl_context.release_lock(table_list.mdl_request.ticket); thd->clear_error(); return res; @@ -3254,8 +3289,20 @@ int get_all_tables(THD *thd, TABLE_LIST *tables, COND *cond) Security_context *sctx= thd->security_ctx; #endif uint table_open_method; + bool can_deadlock; DBUG_ENTER("get_all_tables"); + /* + In cases when SELECT from I_S table being filled by this call is + part of statement which also uses other tables or is being executed + under LOCK TABLES or is part of transaction which also uses other + tables waiting for metadata locks which happens below might result + in deadlocks. + To avoid them we don't wait if conflicting metadata lock is + encountered and skip table with emitting an appropriate warning. + */ + can_deadlock= thd->mdl_context.has_locks(); + lex->view_prepare_mode= TRUE; lex->reset_n_backup_query_tables_list(&query_tables_list_backup); @@ -3275,6 +3322,7 @@ int get_all_tables(THD *thd, TABLE_LIST *tables, COND *cond) if (lsel && lsel->table_list.first) { error= fill_schema_show_cols_or_idxs(thd, tables, schema_table, + can_deadlock, &open_tables_state_backup); goto err; } @@ -3390,7 +3438,8 @@ int get_all_tables(THD *thd, TABLE_LIST *tables, COND *cond) !with_i_schema) { if (!fill_schema_table_from_frm(thd, table, schema_table, db_name, - table_name, schema_table_idx)) + table_name, schema_table_idx, + can_deadlock)) continue; } @@ -3415,7 +3464,8 @@ int get_all_tables(THD *thd, TABLE_LIST *tables, COND *cond) show_table_list->i_s_requested_object= schema_table->i_s_requested_object; res= open_normal_and_derived_tables(thd, show_table_list, - MYSQL_LOCK_IGNORE_FLUSH); + (MYSQL_LOCK_IGNORE_FLUSH | + (can_deadlock ? MYSQL_OPEN_FAIL_ON_MDL_CONFLICT : 0))); lex->sql_command= save_sql_command; /* XXX: show_table_list has a flag i_is_requested,