[MDEV-32236] ALTER TABLE NOWAIT / WAIT can cause table statistics to be wrong Created: 2023-09-24  Updated: 2023-11-28

Status: Stalled
Project: MariaDB Server
Component/s: Data Definition - Alter Table
Affects Version/s: 10.4
Fix Version/s: 10.4, 10.5, 10.6, 10.11, 11.0, 11.1, 11.2

Type: Bug Priority: Critical
Reporter: Michael Widenius Assignee: Oleksandr Byelkin
Resolution: Unresolved Votes: 0
Labels: None


 Description   

ALTER TABLE .. NOWAIT/WAIT is implemented by doing an implicit
SET STATEMENT lock_wait_timeout=#, innodb_lock_wait_timeout=# FOR ALTER ...

This is a unworkable solution because of the following problem:

  • If there is any concurrent SELECT (especially from information schema) or DDL's that updates any of the following tables, there may be lock waits which will cause the tables to not be updated even if the ALTER TABLE was successful:
  • table_stats
  • index_stats
  • column_stats
  • general_log
  • slow_log

At least for the persistent statistics tables the update will be silently ignored, which will result in totally wrong (for example in case of rename of columns or indexes) or stale statistics (in case of removing columns). For DBUG servers, there can be crashes because of lock errors that where not properly handled (as seen by Elena).

Instead of implementing this with SET STATEMENT, the timeout settings should be done inside ALTER TABLE and ONLY be around the execution of the ALTER. The lock timeouts should be restored before any logging calls or calls to delete table statistics.
In 10.6 the table statistics will be deleted after the ALTER TABLE has succeeded, instead of as in 10.4-10.5 when they are updated during initialization of the ALTER TABLE structures, which causes statistics to be deleted even if ALTER TABLE fails/rolls back.



 Comments   
Comment by Oleksandr Byelkin [ 2023-09-29 ]

with help of Serg and Elena guideline for fixing was formulated like this (should be moved in docs)

WAIT/NOWAIT clause of the command should not affect system tables opened implicitly because of internals of the sever implementation (like statistic, routine descriptions, logs and privileges), but affect tables inherited from tables/functions listed in the command (like underlying tables of views, triggers, functions)

Comment by Oleksandr Byelkin [ 2023-10-10 ]

diff --git a/sql/sql_statistics.cc b/sql/sql_statistics.cc
index f27da375562..78a801951b1 100644
--- a/sql/sql_statistics.cc
+++ b/sql/sql_statistics.cc
@@ -3210,6 +3210,8 @@ int read_statistics_for_tables(THD *thd, TABLE_LIST *tables)
   if (open_stat_tables(thd, stat_tables, &open_tables_backup, FALSE))
     DBUG_RETURN(1);
 
+  DEBUG_SYNC(thd, "statistics_read_opened_tables");
+
   for (TABLE_LIST *tl= tables; tl; tl= tl->next_global)
   {
     TABLE_SHARE *table_share;

--source include/have_debug_sync.inc
 
create table t1 (a int);
insert into t1 values (1),(2),(3),(4);
create table t2 (a int);
insert into t2 values (1),(2),(3),(4);
 
set @save_use_stat_tables= @@global.use_stat_tables;
set session use_stat_tables= preferably;
set global use_stat_tables= preferably;
 
ANALYZE TABLE t1,t2 PERSISTENT FOR ALL;
 
connect (con2,localhost,root,,);
SET DEBUG_SYNC='statistics_read_opened_tables SIGNAL ready WAIT_FOR go';
 
--send select * from t1
 
--connection default
 
SET DEBUG_SYNC="now WAIT_FOR ready";
 
drop table t2 NOWAIT;
 
SET DEBUG_SYNC="now SIGNAL go";
 
select 1;
 
--connection con2
--reap
 
--connection default 
SET DEBUG_SYNC='RESET';
 
select * from mysql.table_stats;
show tables;
 
 
drop table t1;
 
set session use_stat_tables= @save_use_stat_tables;
set global use_stat_tables= @save_use_stat_tables;

Here is the code which shows the problem: at the end the t2 table is deleted but statistics about it present.

Comment by Oleksandr Byelkin [ 2023-10-11 ]

Fixed test case which will work even with waiting for stat_tables

set global use_stat_tables= preferably;
 
ANALYZE TABLE t1,t2 PERSISTENT FOR ALL;
 
connect (con3,localhost,root,,);
connect (con2,localhost,root,,);
SET DEBUG_SYNC='statistics_read_opened_tables SIGNAL ready WAIT_FOR go';
 
--send select * from t1
 
--connection default
 
SET DEBUG_SYNC="now WAIT_FOR ready";
 
--send drop table t2 NOWAIT
 
--connection con3
 
select sleep(0.1);
 
SET DEBUG_SYNC="now SIGNAL go";
 
--connection con2
--reap
 
--connection default
--reap
 
--disconnect con2
--disconnect con3
 
SET DEBUG_SYNC='RESET';
 
select * from mysql.table_stats;
show tables;
 
 
drop table t1;
 
set session use_stat_tables= @save_use_stat_tables;
set global use_stat_tables= @save_use_stat_tables;

Comment by Oleksandr Byelkin [ 2023-10-11 ]

After talking to Serg, it should be implemented as handler calls table->file->extra(HA_EXTRA_SKIP_LOCKED) and table->file->extra_opt(HA_EXTRA_LOCK_WAIT, value).

Remove TL_READ_SKIP_LOCKED and TL_WRITE_SKIP_LOCKED

Generated at Thu Feb 08 10:29:51 UTC 2024 using Jira 8.20.16#820016-sha1:9d11dbea5f4be3d4cc21f03a88dd11d8c8687422.