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:
@ -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, ¶m->old_alias,
|
||||
¶m->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, ¶m->old_alias, FRM_ONLY, 0);
|
||||
DBUG_RETURN(-1);
|
||||
}
|
||||
|
||||
if (ha_table_exists(thd, new_db, ¶m->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= ¶m->old_alias;
|
||||
new_alias= ¶m->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, ¶m, 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, ¶m, 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);
|
||||
}
|
||||
|
Reference in New Issue
Block a user