diff --git a/mysql-test/suite/innodb/r/alter_copy_stats.result b/mysql-test/suite/innodb/r/alter_copy_stats.result index b6a04cfa01c61..dcdff3997d5fe 100644 --- a/mysql-test/suite/innodb/r/alter_copy_stats.result +++ b/mysql-test/suite/innodb/r/alter_copy_stats.result @@ -104,12 +104,17 @@ n_rows database_name lower(table_name) DROP TABLE t1; DROP TABLE t2; # -# MDEV-38667 Assertion in diagnostics area on DDL stats timeout +# MDEV-38822 Lock wait timeout does not happen anymore # set @lock_wait_timeout= @@global.innodb_lock_wait_timeout; SET innodb_lock_wait_timeout = 1; CREATE TABLE t ENGINE=InnoDB AS SELECT * FROM mysql.innodb_table_stats; -Warnings: -Warning 1088 Error updating stats for table after table rebuild: Lock wait timeout SET innodb_lock_wait_timeout = @lock_wait_timeout; +SELECT COUNT(*) as n_rows FROM t; +n_rows +1 +# For a deterministic test, we wait for the background thread to update the stats for the new table. +SELECT table_name, n_rows FROM mysql.innodb_table_stats WHERE table_name = 't'; +table_name n_rows +t 1 DROP TABLE t; diff --git a/mysql-test/suite/innodb/t/alter_copy_stats.test b/mysql-test/suite/innodb/t/alter_copy_stats.test index 4a0b905e8e453..04378609d4a89 100644 --- a/mysql-test/suite/innodb/t/alter_copy_stats.test +++ b/mysql-test/suite/innodb/t/alter_copy_stats.test @@ -90,10 +90,15 @@ DROP TABLE t1; DROP TABLE t2; --echo # ---echo # MDEV-38667 Assertion in diagnostics area on DDL stats timeout +--echo # MDEV-38822 Lock wait timeout does not happen anymore --echo # set @lock_wait_timeout= @@global.innodb_lock_wait_timeout; SET innodb_lock_wait_timeout = 1; CREATE TABLE t ENGINE=InnoDB AS SELECT * FROM mysql.innodb_table_stats; SET innodb_lock_wait_timeout = @lock_wait_timeout; +SELECT COUNT(*) as n_rows FROM t; +--echo # For a deterministic test, we wait for the background thread to update the stats for the new table. +--let $wait_condition= SELECT COUNT(*) = 1 FROM mysql.innodb_table_stats WHERE table_name = 't' AND n_rows > 0 +--source include/wait_condition.inc +SELECT table_name, n_rows FROM mysql.innodb_table_stats WHERE table_name = 't'; DROP TABLE t; diff --git a/sql/sql_class.cc b/sql/sql_class.cc index e9de780adbcf3..2cf633296b5b6 100644 --- a/sql/sql_class.cc +++ b/sql/sql_class.cc @@ -511,6 +511,19 @@ int thd_sql_command(const THD *thd) return (int) thd->lex->sql_command; } +extern "C" int thd_sql_is_table_accessed(const MYSQL_THD thd, + const char *db_name, + const char *table_name) +{ + for (TABLE *table= thd->open_tables; table; table= table->next) + { + if (!strcmp(table->s->db.str, db_name) && + !strcmp(table->s->table_name.str, table_name)) + return 1; + } + return 0; +} + /* Returns options used with DDL's, like IF EXISTS etc... Will returns 'nonsense' if the command was not a DDL. diff --git a/storage/innobase/dict/dict0stats_bg.cc b/storage/innobase/dict/dict0stats_bg.cc index b0c34dc6d3040..794ad9f33c925 100644 --- a/storage/innobase/dict/dict0stats_bg.cc +++ b/storage/innobase/dict/dict0stats_bg.cc @@ -97,13 +97,7 @@ static void dict_stats_recalc_pool_deinit() destroy_background_thd(dict_stats_thd); } -/*****************************************************************//** -Add a table to the recalc pool, which is processed by the -background stats gathering thread. Only the table id is added to the -list, so the table can be closed after being enqueued and it will be -opened when needed. If the table does not exist later (has been DROPped), -then it will be removed from the pool and skipped. */ -static void dict_stats_recalc_pool_add(table_id_t id) +void dict_stats_recalc_pool_add(table_id_t id) { ut_ad(!srv_read_only_mode); ut_ad(id); diff --git a/storage/innobase/handler/ha_innodb.cc b/storage/innobase/handler/ha_innodb.cc index 50cfc5570d227..efc500a3ba8c4 100644 --- a/storage/innobase/handler/ha_innodb.cc +++ b/storage/innobase/handler/ha_innodb.cc @@ -128,7 +128,9 @@ TABLE *get_purge_table(THD *thd); TABLE *open_purge_table(THD *thd, const char *db, size_t dblen, const char *tb, size_t tblen); void close_thread_tables(THD* thd); - +extern "C" int thd_sql_is_table_accessed(const MYSQL_THD thd, + const char *db_name, + const char *table_name); #ifdef MYSQL_DYNAMIC_PLUGIN #define tc_size 400 #endif @@ -21299,6 +21301,26 @@ void alter_stats_rebuild(dict_table_t *table, THD *thd) if (!table->space || !dict_stats_is_persistent_enabled(table)) DBUG_VOID_RETURN; + if (thd && thd_sql_command(thd) == SQLCOM_CREATE_TABLE) + { + LEX_STRING *q= thd_query_string(thd); + if (q->str && q->length > 0) + { + + if ((thd_sql_is_table_accessed(thd, "mysql", "innodb_table_stats")) || + thd_sql_is_table_accessed(thd, "mysql", "innodb_index_stats")) + { + // Avoids a deadlock where a shared lock is held on the + // stats system table, then dict_stats_update tries to acquire + // an exclusive lock on it. An example is "CREATE TABLE t1 AS SELECT * + // FROM innodb_[table/index]_stats". + dict_stats_recalc_pool_add(table->id); + + DBUG_VOID_RETURN; + } + } + } + dberr_t ret= dict_stats_update(table, DICT_STATS_RECALC_PERSISTENT); if (ret != DB_SUCCESS) push_warning_printf(thd, Sql_condition::WARN_LEVEL_WARN, diff --git a/storage/innobase/include/dict0stats.h b/storage/innobase/include/dict0stats.h index 3b006daf53c5c..a662ee2c9385b 100644 --- a/storage/innobase/include/dict0stats.h +++ b/storage/innobase/include/dict0stats.h @@ -245,3 +245,12 @@ dict_stats_empty_table( dict_table_t* table, bool empty_defrag_stats); #endif /* dict0stats_h */ + +/** + * @brief Add a table to the recalc pool, which is processed by the + * background stats gathering thread. Only the table id is added to the + * list, so the table can be closed after being enqueued and it will be + * opened when needed. If the table does not exist later (has been DROPped), + * then it will be removed from the pool and skipped. + */ +void dict_stats_recalc_pool_add(table_id_t id);