1
0
mirror of https://github.com/MariaDB/server.git synced 2025-07-29 05:21:33 +03:00

MDEV-23842 Atomic RENAME TABLE

- Major rewrite of ddl_log.cc and ddl_log.h
  - ddl_log.cc described in the beginning how the recovery works.
  - ddl_log.log has unique signature and is dynamic. It's easy to
    add more information to the header and other ddl blocks while still
    being able to execute old ddl entries.
  - IO_SIZE for ddl blocks is now dynamic. Can be changed without affecting
    recovery of old logs.
  - Code is more modular and is now usable outside of partition handling.
  - Renamed log file to dll_recovery.log and added option --log-ddl-recovery
    to allow one to specify the path & filename.
- Added ddl_log_entry_phase[], number of phases for each DDL action,
  which allowed me to greatly simply set_global_from_ddl_log_entry()
- Changed how strings are stored in log entries, which allows us to
  store much more information in a log entry.
- ddl log is now always created at start and deleted on normal shutdown.
  This simplices things notable.
- Added probes debug_crash_here() and debug_simulate_error() to simply
  crash testing and allow crash after a given number of times a probe
  is executed. See comments in debug_sync.cc and rename_table.test for
  how this can be used.
- Reverting failed table and view renames is done trough the ddl log.
  This ensures that the ddl log is tested also outside of recovery.
- Added helper function 'handler::needs_lower_case_filenames()'
- Extend binary log with Q_XID events. ddl log handling is using this
  to check if a ddl log entry was logged to the binary log (if yes,
  it will be deleted from the log during ddl_log_close_binlogged_events()
- If a DDL entry fails 3 time, disable it. This is to ensure that if
  we have a crash in ddl recovery code the server will not get stuck
  in a forever crash-restart-crash loop.

mysqltest.cc changes:
- --die will now replace $variables with their values
- $error will contain the error of the last failed statement

storage engine changes:
- maria_rename() was changed to be more robust against crashes during
  rename.
This commit is contained in:
Monty
2020-10-15 02:25:57 +03:00
committed by Sergei Golubchik
parent 55c771b4f3
commit 47010ccffa
69 changed files with 4121 additions and 1403 deletions

View File

@ -30,17 +30,20 @@
#include "sql_base.h" // tdc_remove_table, lock_table_names,
#include "sql_handler.h" // mysql_ha_rm_tables
#include "sql_statistics.h"
#include "ddl_log.h"
#include "debug_sync.h"
static TABLE_LIST *rename_tables(THD *thd, TABLE_LIST *table_list,
bool skip_error, bool if_exits,
bool *force_if_exists);
static bool do_rename(THD *thd, TABLE_LIST *ren_table,
const LEX_CSTRING *new_db,
const LEX_CSTRING *new_table_name,
const LEX_CSTRING *new_table_alias,
bool skip_error, bool if_exists, bool *force_if_exists);
/* used to hold table entries for as part of list of renamed temporary tables */
struct TABLE_PAIR
{
TABLE_LIST *from, *to;
};
static TABLE_LIST *reverse_table_list(TABLE_LIST *table_list);
static bool rename_tables(THD *thd, TABLE_LIST *table_list,
DDL_LOG_STATE *ddl_log_state,
bool skip_error, bool if_exits,
bool *force_if_exists);
/*
Every two entries in the table_list form a pair of original name and
@ -55,6 +58,7 @@ bool mysql_rename_tables(THD *thd, TABLE_LIST *table_list, bool silent,
TABLE_LIST *ren_table= 0;
int to_table;
const char *rename_log_table[2]= {NULL, NULL};
DDL_LOG_STATE ddl_log_state;
DBUG_ENTER("mysql_rename_tables");
/*
@ -151,32 +155,14 @@ bool mysql_rename_tables(THD *thd, TABLE_LIST *table_list, bool silent,
goto err;
error=0;
bzero(&ddl_log_state, sizeof(ddl_log_state));
/*
An exclusive lock on table names is satisfactory to ensure
no other thread accesses this table.
*/
if ((ren_table= rename_tables(thd, table_list, 0, if_exists,
&force_if_exists)))
{
/* Rename didn't succeed; rename back the tables in reverse order */
TABLE_LIST *table;
/* Reverse the table list */
table_list= reverse_table_list(table_list);
/* Find the last renamed table */
for (table= table_list;
table->next_local != ren_table ;
table= table->next_local->next_local) ;
table= table->next_local->next_local; // Skip error table
/* Revert to old names */
rename_tables(thd, table, 1, if_exists, &force_if_exists);
/* Revert the table list (for prepared statements) */
table_list= reverse_table_list(table_list);
error= 1;
}
error= rename_tables(thd, table_list, &ddl_log_state,
0, if_exists, &force_if_exists);
if (likely(!silent && !error))
{
@ -186,49 +172,43 @@ bool mysql_rename_tables(THD *thd, TABLE_LIST *table_list, bool silent,
/* Add IF EXISTS to binary log */
thd->variables.option_bits|= OPTION_IF_EXISTS;
}
debug_crash_here("ddl_log_rename_before_binlog");
/*
Store xid in ddl log and binary log so that we can check on ddl recovery
if the item is in the binary log (and thus the operation was complete
*/
thd->binlog_xid= thd->query_id;
ddl_log_update_xid(&ddl_log_state, thd->binlog_xid);
binlog_error= write_bin_log(thd, TRUE, thd->query(), thd->query_length());
if (binlog_error)
error= 1;
thd->binlog_xid= 0;
thd->variables.option_bits= save_option_bits;
debug_crash_here("ddl_log_rename_after_binlog");
if (likely(!binlog_error))
my_ok(thd);
}
if (likely(!error))
{
query_cache_invalidate3(thd, table_list, 0);
ddl_log_complete(&ddl_log_state);
}
else
{
/* Revert the renames of normal tables with the help of the ddl log */
ddl_log_revert(thd, &ddl_log_state);
}
err:
DBUG_RETURN(error || binlog_error);
}
/*
reverse table list
SYNOPSIS
reverse_table_list()
table_list pointer to table _list
RETURN
pointer to new (reversed) list
*/
static TABLE_LIST *reverse_table_list(TABLE_LIST *table_list)
{
TABLE_LIST *prev= 0;
while (table_list)
{
TABLE_LIST *next= table_list->next_local;
table_list->next_local= prev;
prev= table_list;
table_list= next;
}
return (prev);
}
static bool
do_rename_temporary(THD *thd, TABLE_LIST *ren_table, TABLE_LIST *new_table,
bool skip_error)
do_rename_temporary(THD *thd, TABLE_LIST *ren_table, TABLE_LIST *new_table)
{
LEX_CSTRING *new_alias;
DBUG_ENTER("do_rename_temporary");
@ -242,12 +222,94 @@ do_rename_temporary(THD *thd, TABLE_LIST *ren_table, TABLE_LIST *new_table,
DBUG_RETURN(1); // This can't be skipped
}
DBUG_RETURN(thd->rename_temporary_table(ren_table->table,
&new_table->db, new_alias));
}
/**
Parameters for do_rename
*/
struct rename_param
{
LEX_CSTRING old_alias, new_alias;
handlerton *from_table_hton;
};
/**
check_rename()
Check pre-conditions for rename
- From table should exists
- To table should not exists.
SYNOPSIS
@param new_table_name The new table/view name
@param new_table_alias The new table/view alias
@param if_exists If not set, give an error if the table does not
exists. If set, just give a warning in this case.
@return
@retval 0 ok
@retval >0 Error (from table doesn't exists or to table exists)
@retval <0 Can't do rename, but no error
*/
static int
check_rename(THD *thd, rename_param *param,
TABLE_LIST *ren_table,
const LEX_CSTRING *new_db,
const LEX_CSTRING *new_table_name,
const LEX_CSTRING *new_table_alias,
bool if_exists)
{
DBUG_ENTER("check_rename");
DBUG_PRINT("enter", ("if_exists: %d", (int) if_exists));
if (lower_case_table_names == 2)
{
param->old_alias= ren_table->alias;
param->new_alias= *new_table_alias;
}
else
{
param->old_alias= ren_table->table_name;
param->new_alias= *new_table_name;
}
DBUG_ASSERT(param->new_alias.str);
if (!ha_table_exists(thd, &ren_table->db, &param->old_alias,
&param->from_table_hton) ||
!param->from_table_hton)
{
my_error(ER_NO_SUCH_TABLE, MYF(if_exists ? ME_NOTE : 0),
ren_table->db.str, param->old_alias.str);
DBUG_RETURN(if_exists ? -1 : 1);
}
if (param->from_table_hton != view_pseudo_hton &&
ha_check_if_updates_are_ignored(thd, param->from_table_hton, "RENAME"))
{
/*
Shared table. Just drop the old .frm as it's not correct anymore
Discovery will find the old table when it's accessed
*/
tdc_remove_table(thd, ren_table->db.str, ren_table->table_name.str);
quick_rm_table(thd, 0, &ren_table->db, &param->old_alias, FRM_ONLY, 0);
DBUG_RETURN(-1);
}
if (ha_table_exists(thd, new_db, &param->new_alias, 0))
{
my_error(ER_TABLE_EXISTS_ERROR, MYF(0), param->new_alias.str);
DBUG_RETURN(1); // This can't be skipped
}
DBUG_RETURN(0);
}
/*
Rename a single table or a view
@ -256,15 +318,13 @@ do_rename_temporary(THD *thd, TABLE_LIST *ren_table, TABLE_LIST *new_table,
thd Thread handle
ren_table A table/view to be renamed
new_db The database to which the table to be moved to
new_table_name The new table/view name
new_table_alias The new table/view alias
skip_error Whether to skip error
if_exists Skip error, but only if the table didn't exists
skip_error Skip error, but only if the table didn't exists
force_if_exists Set to 1 if we have to log the query with 'IF EXISTS'
Otherwise don't touch the value
DESCRIPTION
Rename a single table or a view.
In case of failure, all changes will be reverted
RETURN
false Ok
@ -272,54 +332,19 @@ do_rename_temporary(THD *thd, TABLE_LIST *ren_table, TABLE_LIST *new_table,
*/
static bool
do_rename(THD *thd, TABLE_LIST *ren_table, const LEX_CSTRING *new_db,
const LEX_CSTRING *new_table_name,
const LEX_CSTRING *new_table_alias,
bool skip_error, bool if_exists, bool *force_if_exists)
do_rename(THD *thd, rename_param *param, DDL_LOG_STATE *ddl_log_state,
TABLE_LIST *ren_table, const LEX_CSTRING *new_db,
bool skip_error, bool *force_if_exists)
{
int rc= 1;
handlerton *hton, *new_hton;
LEX_CSTRING old_alias, new_alias;
handlerton *hton;
LEX_CSTRING *old_alias, *new_alias;
DBUG_ENTER("do_rename");
DBUG_PRINT("enter", ("skip_error: %d if_exists: %d", (int) skip_error,
(int) if_exists));
DBUG_PRINT("enter", ("skip_error: %d", (int) skip_error));
if (lower_case_table_names == 2)
{
old_alias= ren_table->alias;
new_alias= *new_table_alias;
}
else
{
old_alias= ren_table->table_name;
new_alias= *new_table_name;
}
DBUG_ASSERT(new_alias.str);
if (!ha_table_exists(thd, &ren_table->db, &old_alias, &hton) || !hton)
{
my_error(ER_NO_SUCH_TABLE, MYF((skip_error | if_exists) ? ME_NOTE : 0),
ren_table->db.str, old_alias.str);
DBUG_RETURN(skip_error || if_exists ? 0 : 1);
}
if (hton != view_pseudo_hton &&
ha_check_if_updates_are_ignored(thd, hton, "RENAME"))
{
/*
Shared table. Just drop the old .frm as it's not correct anymore
Discovery will find the old table when it's accessed
*/
tdc_remove_table(thd, ren_table->db.str, ren_table->table_name.str);
quick_rm_table(thd, 0, &ren_table->db, &old_alias, FRM_ONLY, 0);
DBUG_RETURN(0);
}
if (ha_table_exists(thd, new_db, &new_alias, &new_hton))
{
my_error(ER_TABLE_EXISTS_ERROR, MYF(0), new_alias.str);
DBUG_RETURN(1); // This can't be skipped
}
old_alias= &param->old_alias;
new_alias= &param->new_alias;
hton= param->from_table_hton;
DBUG_ASSERT(!thd->locked_tables_mode);
@ -337,17 +362,36 @@ do_rename(THD *thd, TABLE_LIST *ren_table, const LEX_CSTRING *new_db,
*force_if_exists= 1;
thd->replication_flags= 0;
if (!(rc= mysql_rename_table(hton, &ren_table->db, &old_alias,
new_db, &new_alias, 0)))
if (ddl_log_rename_table(thd, ddl_log_state, hton,
&ren_table->db, old_alias, new_db, new_alias))
DBUG_RETURN(1);
debug_crash_here("ddl_log_rename_before_rename_table");
if (!(rc= mysql_rename_table(hton, &ren_table->db, old_alias,
new_db, new_alias, 0)))
{
(void) rename_table_in_stat_tables(thd, &ren_table->db,
&ren_table->table_name,
new_db, &new_alias);
if ((rc= Table_triggers_list::change_table_name(thd, &ren_table->db,
&old_alias,
&ren_table->table_name,
new_db,
&new_alias)))
/* Table rename succeded.
It's safe to start recovery at rename trigger phase
*/
debug_crash_here("ddl_log_rename_before_phase_trigger");
ddl_log_update_phase(ddl_log_state, DDL_RENAME_PHASE_TRIGGER);
debug_crash_here("ddl_log_rename_before_rename_trigger");
if (!(rc= Table_triggers_list::change_table_name(thd, &ren_table->db,
old_alias,
&ren_table->table_name,
new_db,
new_alias)))
{
debug_crash_here("ddl_log_rename_before_stat_tables");
(void) rename_table_in_stat_tables(thd, &ren_table->db,
&ren_table->table_name,
new_db, new_alias);
debug_crash_here("ddl_log_rename_after_stat_tables");
}
else
{
/*
We've succeeded in renaming table's .frm and in updating
@ -355,8 +399,12 @@ do_rename(THD *thd, TABLE_LIST *ren_table, const LEX_CSTRING *new_db,
triggers appropriately. So let us revert operations on .frm
and handler's data and report about failure to rename table.
*/
(void) mysql_rename_table(hton, new_db, &new_alias,
&ren_table->db, &old_alias, NO_FK_CHECKS);
debug_crash_here("ddl_log_rename_after_failed_rename_trigger");
(void) mysql_rename_table(hton, new_db, new_alias,
&ren_table->db, old_alias, NO_FK_CHECKS);
debug_crash_here("ddl_log_rename_after_revert_rename_table");
ddl_log_disable_entry(ddl_log_state);
debug_crash_here("ddl_log_rename_after_disable_entry");
}
}
if (thd->replication_flags & OPTION_IF_EXISTS)
@ -371,9 +419,25 @@ do_rename(THD *thd, TABLE_LIST *ren_table, const LEX_CSTRING *new_db,
*/
if (thd->lex->sql_command != SQLCOM_ALTER_DB_UPGRADE &&
cmp(&ren_table->db, new_db))
{
my_error(ER_FORBID_SCHEMA_CHANGE, MYF(0), ren_table->db.str, new_db->str);
else
rc= mysql_rename_view(thd, new_db, &new_alias, ren_table);
DBUG_RETURN(1);
}
ddl_log_rename_view(thd, ddl_log_state, &ren_table->db,
&ren_table->table_name, new_db, new_alias);
debug_crash_here("ddl_log_rename_before_rename_view");
rc= mysql_rename_view(thd, new_db, new_alias, &ren_table->db,
&ren_table->table_name);
debug_crash_here("ddl_log_rename_after_rename_view");
if (rc)
{
/*
On error mysql_rename_view() will leave things as such.
*/
ddl_log_disable_entry(ddl_log_state);
debug_crash_here("ddl_log_rename_after_disable_entry");
}
}
DBUG_RETURN(rc && !skip_error ? 1 : 0);
}
@ -391,6 +455,7 @@ do_rename(THD *thd, TABLE_LIST *ren_table, const LEX_CSTRING *new_db,
rename_tables()
thd Thread handle
table_list List of tables to rename
ddl_log_state ddl logging
skip_error Whether to skip errors
if_exists Don't give an error if table doesn't exists
force_if_exists Set to 1 if we have to log the query with 'IF EXISTS'
@ -403,14 +468,16 @@ do_rename(THD *thd, TABLE_LIST *ren_table, const LEX_CSTRING *new_db,
RETURN
0 Ok
table pointer to the table list element which rename failed
1 error
All tables are reverted to their original names
*/
static TABLE_LIST *
rename_tables(THD *thd, TABLE_LIST *table_list, bool skip_error,
bool if_exists, bool *force_if_exists)
static bool
rename_tables(THD *thd, TABLE_LIST *table_list, DDL_LOG_STATE *ddl_log_state,
bool skip_error, bool if_exists, bool *force_if_exists)
{
TABLE_LIST *ren_table, *new_table;
List<TABLE_PAIR> tmp_tables;
DBUG_ENTER("rename_tables");
*force_if_exists= 0;
@ -419,11 +486,48 @@ rename_tables(THD *thd, TABLE_LIST *table_list, bool skip_error,
{
new_table= ren_table->next_local;
if (is_temporary_table(ren_table) ?
do_rename_temporary(thd, ren_table, new_table, skip_error) :
do_rename(thd, ren_table, &new_table->db, &new_table->table_name,
&new_table->alias, skip_error, if_exists, force_if_exists))
DBUG_RETURN(ren_table);
if (is_temporary_table(ren_table))
{
/*
Store renamed temporary tables into a list.
We don't store these in the ddl log to avoid writes and syncs
when only using temporary tables. We don't need the log as
all temporary tables will disappear anyway in a crash.
*/
TABLE_PAIR *pair= (TABLE_PAIR*) thd->alloc(sizeof(*pair));
if (! pair || tmp_tables.push_front(pair, thd->mem_root))
goto revert_rename;
pair->from= ren_table;
pair->to= new_table;
if (do_rename_temporary(thd, ren_table, new_table))
goto revert_rename;
}
else
{
int error;
rename_param param;
error= check_rename(thd, &param, ren_table, &new_table->db,
&new_table->table_name,
&new_table->alias, (skip_error || if_exists));
if (error < 0)
continue; // Ignore rename (if exists)
if (error > 0)
goto revert_rename;
if (do_rename(thd, &param, ddl_log_state,
ren_table, &new_table->db,
skip_error, force_if_exists))
goto revert_rename;
}
}
DBUG_RETURN(0);
revert_rename:
/* Revert temporary tables. Normal tables are reverted in the caller */
List_iterator_fast<TABLE_PAIR> it(tmp_tables);
while (TABLE_PAIR *pair= it++)
do_rename_temporary(thd, pair->to, pair->from);
DBUG_RETURN(1);
}