Commit ffe7f19f authored by Monty's avatar Monty Committed by Sergei Golubchik

MDEV-24746 Atomic CREATE TRIGGER

The purpose of this task is to ensure that CREATE TRIGGER is atomic

When a trigger is created, we first create a trigger_name.TRN file and then
create or update the table_name.TRG files.
This is done by creating .TRN~ and .TRG~ files and replacing (or creating)
the result files.

The new logic is

- Log CREATE TRIGGER to DDL log, with a marker if old trigger existsted
- If old .TRN or .TRG files exists, make backup copies of these
- Create the new .TRN and .TRG files as before
- Remove the backups

Crash recovery
- If query has been logged to binary log:
  - delete any left over backup files
- else
   - Delete any old .TRN~ or .TRG~ files
   - If there was orignally some triggers (old .TRG file existed)
      - If we crashed before creating all backup files
         - Delete existing backup files
      - else
         - Restore backup files
      - end
   - Delete .TRN and .TRG file (as there was no triggers before

One benefit of the new code is that CREATE OR REPLACE TRIGGER is now
totally atomic even if there existed an old trigger: Either the old
trigger will be replaced or the old one will be left untouched.

Other things:
- If sql_create_definition_file() would fail, there could be memory leaks
  in CREATE TRIGGER, DROP TRIGGER or CREATE OR REPLACE TRIGGER.  This
  is now fixed.
parent d494abd1
To debug a the ddl_recovery code in a failing ddl_recovery test one could do
the following:
- Add # before --exec echo "restart" ...
- Force $e (engine), $c (crash point) and $r (crash position) to the values
where things goes wrong. See comments in alter_table.test for how to do this.
- start mariadbd in a debugger
run the following in the debugger
(Replace 'atomic.create_trigger' with the failing test case)
#break ha_recover
#break MYSQL_BIN_LOG::recover
#break MYSQL_BIN_LOG::open
break ddl_log_close_binlogged_events
break ddl_log_execute_action
break ddl_log_execute_recovery
run --datadir=/my/maria-10.6/mysql-test/var/log/atomic.create_trigger/mysqld.1/data --log-basename=master --log-bin-index=mysqld-bin.index --debug --log-bin
This diff is collapsed.
--source include/have_debug.inc
--source include/have_log_bin.inc
--source include/not_valgrind.inc
#
# Testing of atomic CREATE TRIGGER with crashes in a lot of different places
#
let $MYSQLD_DATADIR= `SELECT @@datadir`;
let $engine_count=1;
let $engines='aria';
let $crash_count=6;
let $crash_points='ddl_log_create_before_create_trigger', 'ddl_log_create_after_create_trigger', 'definition_file_after_create', 'ddl_log_drop_before_binlog', 'ddl_log_drop_after_binlog','ddl_log_drop_before_delete_tmp';
let $old_debug=`select @@debug_dbug`;
let $e=0;
let $keep_include_silent=1;
let $grep_script=CREATE.*TRIGGER;
let $drops=3;
--disable_query_log
while ($e < $engine_count)
{
inc $e;
let $engine=`select ELT($e, $engines)`;
let $default_engine=$engine;
let $extra_option=;
if ($engine == "aria")
{
let $extra_option=transactional=1;
}
if ($engine == "aria_notrans")
{
let $default_engine="aria";
let $extra_option=transactional=0;
}
--eval set @@default_storage_engine=$default_engine
--eval create table t1 (a int not null, b int not null) $extra_option;
insert into t1 values(1,1);
flush tables;
let $c=0;
while ($c < $crash_count)
{
inc $c;
let $crash=`select ELT($c, $crash_points)`;
let $r=0;
while ($r < $drops)
{
inc $r;
RESET MASTER;
echo "engine: $engine crash point: $crash position: $r";
--exec echo "restart" > $MYSQLTEST_VARDIR/tmp/mysqld.1.expect
--disable_reconnect
--eval set @@debug_dbug="+d,$crash",@debug_crash_counter=$r
let $errno=0;
delimiter |;
--error 0,2013
CREATE TRIGGER t1_trg before insert on t1 for each row
begin
if isnull(new.a) then
set new.a:= 1000;
end if;
end|
delimiter ;|
let $error=$errno;
if ($error == 0)
{
delimiter |;
--error 0,2013
CREATE OR REPLACE TRIGGER t2_trg before insert on t1 for each row
begin
if isnull(new.b) then
set new.b:= 2000;
end if;
end|
delimiter ;|
let $error=$errno;
}
if ($error == 0)
{
delimiter |;
--error 0,2013
CREATE OR REPLACE TRIGGER t2_trg before insert on t1 for each row
begin
if isnull(new.b) then
set new.b:= 3000;
end if;
end|
delimiter ;|
let $error=$errno;
}
--enable_reconnect
--source include/wait_until_connected_again.inc
--disable_query_log
--eval set @@debug_dbug="$old_debug"
if ($error == 0)
{
echo "No crash!";
}
# Check which tables still exists
--list_files $MYSQLD_DATADIR/test *TR*
--list_files $MYSQLD_DATADIR/test *sql*
--replace_column 7 #
--error 0,ER_TRG_DOES_NOT_EXIST
SHOW CREATE TRIGGER t1_trg;
--replace_column 7 #
--error 0,ER_TRG_DOES_NOT_EXIST
SHOW CREATE TRIGGER t2_trg;
--let $binlog_file=master-bin.000001
--source include/show_binlog_events.inc
--disable_warnings
drop trigger if exists t1_trg;
drop trigger if exists t2_trg;
--enable_warnings
}
}
}
drop table t1;
--enable_query_log
"position: 1"
"position: 2"
"position: 3"
t1.TRG
t1_trg.TRN
Trigger sql_mode SQL Original Statement character_set_client collation_connection Database Collation Created
t1_trg STRICT_TRANS_TABLES,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION CREATE DEFINER=`root`@`localhost` trigger t1_trg before insert on t1 for each row
begin
if isnull(new.a) then
set new.a:= 1000;
end if;
end latin1 latin1_swedish_ci latin1_swedish_ci #
"position: 4"
t1.TRG
t1_trg.TRN
Trigger sql_mode SQL Original Statement character_set_client collation_connection Database Collation Created
t1_trg STRICT_TRANS_TABLES,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION CREATE DEFINER=`root`@`localhost` trigger t1_trg before insert on t1 for each row
begin
if isnull(new.a) then
set new.a:= 1000;
end if;
end latin1 latin1_swedish_ci latin1_swedish_ci #
"position: 5"
t1.TRG
t1_trg.TRN
t2_trg.TRN
Trigger sql_mode SQL Original Statement character_set_client collation_connection Database Collation Created
t1_trg STRICT_TRANS_TABLES,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION CREATE DEFINER=`root`@`localhost` trigger t1_trg before insert on t1 for each row
begin
if isnull(new.a) then
set new.a:= 1000;
end if;
end latin1 latin1_swedish_ci latin1_swedish_ci #
Trigger sql_mode SQL Original Statement character_set_client collation_connection Database Collation Created
t2_trg STRICT_TRANS_TABLES,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION CREATE DEFINER=`root`@`localhost` trigger t2_trg before insert on t1 for each row
begin
if isnull(new.b) then
set new.b:= 2000;
end if;
end latin1 latin1_swedish_ci latin1_swedish_ci #
"position: 6"
t1.TRG
t1_trg.TRN
t2_trg.TRN
Trigger sql_mode SQL Original Statement character_set_client collation_connection Database Collation Created
t1_trg STRICT_TRANS_TABLES,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION CREATE DEFINER=`root`@`localhost` trigger t1_trg before insert on t1 for each row
begin
if isnull(new.a) then
set new.a:= 1000;
end if;
end latin1 latin1_swedish_ci latin1_swedish_ci #
Trigger sql_mode SQL Original Statement character_set_client collation_connection Database Collation Created
t2_trg STRICT_TRANS_TABLES,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION CREATE DEFINER=`root`@`localhost` trigger t2_trg before insert on t1 for each row
begin
if isnull(new.b) then
set new.b:= 2000;
end if;
end latin1 latin1_swedish_ci latin1_swedish_ci #
"position: 7"
t1.TRG
t1_trg.TRN
t2_trg.TRN
Trigger sql_mode SQL Original Statement character_set_client collation_connection Database Collation Created
t1_trg STRICT_TRANS_TABLES,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION CREATE DEFINER=`root`@`localhost` trigger t1_trg before insert on t1 for each row
begin
if isnull(new.a) then
set new.a:= 1000;
end if;
end latin1 latin1_swedish_ci latin1_swedish_ci #
Trigger sql_mode SQL Original Statement character_set_client collation_connection Database Collation Created
t2_trg STRICT_TRANS_TABLES,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION CREATE DEFINER=`root`@`localhost` trigger t2_trg before insert on t1 for each row
begin
if isnull(new.b) then
set new.b:= 2000;
end if;
end latin1 latin1_swedish_ci latin1_swedish_ci #
"position: 8"
t1.TRG
t1_trg.TRN
t2_trg.TRN
Trigger sql_mode SQL Original Statement character_set_client collation_connection Database Collation Created
t1_trg STRICT_TRANS_TABLES,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION CREATE DEFINER=`root`@`localhost` trigger t1_trg before insert on t1 for each row
begin
if isnull(new.a) then
set new.a:= 1000;
end if;
end latin1 latin1_swedish_ci latin1_swedish_ci #
Trigger sql_mode SQL Original Statement character_set_client collation_connection Database Collation Created
t2_trg STRICT_TRANS_TABLES,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION CREATE DEFINER=`root`@`localhost` trigger t2_trg before insert on t1 for each row
begin
if isnull(new.b) then
set new.b:= 3000;
end if;
end latin1 latin1_swedish_ci latin1_swedish_ci #
--source include/have_debug.inc
#
# Testing of atomic CREATE TRIGGER when write fails in create_definition_file
#
let $MYSQLD_DATADIR= `SELECT @@datadir`;
let $engine_count=1;
let $engines='aria';
let $old_debug=`select @@debug_dbug`;
let $e=0;
--disable_query_log
create table t1 (a int not null, b int not null);
insert into t1 values(1,1);
flush tables;
# sql_create_definition_file is called twice per CREATE TRIGGER and 1 more
# in case we drop an existing trigger, so we need to test 3*2 +1 failures
# and also when there is no failures (= 8)
let $try_count=8;
let $r=0;
while ($r < $try_count)
{
inc $r;
echo "position: $r";
--eval set @@debug_dbug="+d,definition_file_simulate_write_error",@debug_error_counter=$r;
let $errno=0;
delimiter |;
--error 0,3
create trigger t1_trg before insert on t1 for each row
begin
if isnull(new.a) then
set new.a:= 1000;
end if;
end|
delimiter ;|
let $error=$errno;
if ($error == 0)
{
delimiter |;
--error 0,3
create or replace trigger t2_trg before insert on t1 for each row
begin
if isnull(new.b) then
set new.b:= 2000;
end if;
end|
delimiter ;|
let $error=$errno;
}
if ($error == 0)
{
delimiter |;
--error 0,3
create or replace trigger t2_trg before insert on t1 for each row
begin
if isnull(new.b) then
set new.b:= 3000;
end if;
end|
delimiter ;|
let $error=$errno;
}
--eval set @@debug_dbug="$old_debug"
# Check which tables still exists
--list_files $MYSQLD_DATADIR/test *TR*
--list_files $MYSQLD_DATADIR/test *sql*
--replace_column 7 #
--error 0,ER_TRG_DOES_NOT_EXIST
SHOW CREATE TRIGGER t1_trg;
--replace_column 7 #
--error 0,ER_TRG_DOES_NOT_EXIST
SHOW CREATE TRIGGER t2_trg;
--disable_warnings
drop trigger if exists t1_trg;
drop trigger if exists t2_trg;
--enable_warnings
}
drop table t1;
--enable_query_log
......@@ -90,7 +90,7 @@ const char *ddl_log_action_name[DDL_LOG_LAST_ACTION]=
"rename table", "rename view",
"initialize drop table", "drop table",
"drop view", "drop trigger", "drop db", "create table", "create view",
"delete tmp file",
"delete tmp file", "create trigger",
};
/* Number of phases per entry */
......@@ -100,7 +100,7 @@ const uchar ddl_log_entry_phases[DDL_LOG_LAST_ACTION]=
(uchar) EXCH_PHASE_END, (uchar) DDL_RENAME_PHASE_END, 1, 1,
(uchar) DDL_DROP_PHASE_END, 1, 1,
(uchar) DDL_DROP_DB_PHASE_END, (uchar) DDL_CREATE_TABLE_PHASE_END,
(uchar) DDL_CREATE_VIEW_PHASE_END, 0,
(uchar) DDL_CREATE_VIEW_PHASE_END, 0, (uchar) DDL_CREATE_TRIGGER_PHASE_END,
};
......@@ -1637,7 +1637,96 @@ static int ddl_log_execute_action(THD *thd, MEM_ROOT *mem_root,
(void) update_phase(entry_pos, DDL_LOG_FINAL_PHASE);
break;
}
break;
case DDL_LOG_CREATE_TRIGGER_ACTION:
{
LEX_CSTRING db, table, trigger;
db= ddl_log_entry->db;
table= ddl_log_entry->name;
trigger= ddl_log_entry->tmp_name;
/* Delete backup .TRG (trigger file) if it exists */
(void) build_filename_and_delete_tmp_file(to_path, sizeof(to_path) - 1,
&db, &table,
TRG_EXT,
key_file_fileparser);
(void) build_filename_and_delete_tmp_file(to_path, sizeof(to_path) - 1,
&db, &trigger,
TRN_EXT,
key_file_fileparser);
switch (ddl_log_entry->phase) {
case DDL_CREATE_TRIGGER_PHASE_DELETE_COPY:
{
size_t length;
/* Delete copy of .TRN and .TRG files */
length= build_table_filename(to_path, sizeof(to_path) - 1,
db.str, table.str, TRG_EXT, 0);
to_path[length]= '-';
to_path[length+1]= 0;
mysql_file_delete(key_file_fileparser, to_path,
MYF(MY_WME|MY_IGNORE_ENOENT));
length= build_table_filename(to_path, sizeof(to_path) - 1,
db.str, trigger.str, TRN_EXT, 0);
to_path[length]= '-';
to_path[length+1]= 0;
mysql_file_delete(key_file_fileparser, to_path,
MYF(MY_WME|MY_IGNORE_ENOENT));
}
/* Nothing else to do */
(void) update_phase(entry_pos, DDL_LOG_FINAL_PHASE);
break;
case DDL_CREATE_TRIGGER_PHASE_OLD_COPIED:
{
LEX_CSTRING path= {to_path, 0};
size_t length;
/* Restore old version if the .TRN and .TRG files */
length= build_table_filename(to_path, sizeof(to_path) - 1,
db.str, table.str, TRG_EXT, 0);
to_path[length]='-';
to_path[length+1]= 0;
path.length= length+1;
/* an old TRN file only exist in the case if REPLACE was used */
if (!access(to_path, F_OK))
sql_restore_definition_file(&path);
length= build_table_filename(to_path, sizeof(to_path) - 1,
db.str, trigger.str, TRN_EXT, 0);
to_path[length]='-';
to_path[length+1]= 0;
path.length= length+1;
if (!access(to_path, F_OK))
sql_restore_definition_file(&path);
else
{
/*
There was originally no .TRN for this trigger.
Delete the newly created one.
*/
to_path[length]= 0;
mysql_file_delete(key_file_fileparser, to_path,
MYF(MY_WME|MY_IGNORE_ENOENT));
}
(void) update_phase(entry_pos, DDL_LOG_FINAL_PHASE);
break;
}
case DDL_CREATE_TRIGGER_PHASE_NO_OLD_TRIGGER:
{
/* No old trigger existed. We can just delete the .TRN and .TRG files */
build_table_filename(to_path, sizeof(to_path) - 1,
db.str, table.str, TRG_EXT, 0);
mysql_file_delete(key_file_fileparser, to_path,
MYF(MY_WME|MY_IGNORE_ENOENT));
build_table_filename(to_path, sizeof(to_path) - 1,
db.str, trigger.str, TRN_EXT, 0);
mysql_file_delete(key_file_fileparser, to_path,
MYF(MY_WME|MY_IGNORE_ENOENT));
(void) update_phase(entry_pos, DDL_LOG_FINAL_PHASE);
break;
}
}
break;
}
}
default:
DBUG_ASSERT(0);
break;
......@@ -2633,3 +2722,25 @@ bool ddl_log_delete_tmp_file(THD *thd, DDL_LOG_STATE *ddl_state,
ddl_log_entry.unique_id= depending_state->execute_entry->entry_pos;
DBUG_RETURN(ddl_log_write(ddl_state, &ddl_log_entry));
}
/**
Log CREATE TRIGGER
*/
bool ddl_log_create_trigger(THD *thd, DDL_LOG_STATE *ddl_state,
const LEX_CSTRING *db, const LEX_CSTRING *table,
const LEX_CSTRING *trigger_name,
enum_ddl_log_create_trigger_phase phase)
{
DDL_LOG_ENTRY ddl_log_entry;
DBUG_ENTER("ddl_log_create_view");
bzero(&ddl_log_entry, sizeof(ddl_log_entry));
ddl_log_entry.action_type= DDL_LOG_CREATE_TRIGGER_ACTION;
ddl_log_entry.db= *const_cast<LEX_CSTRING*>(db);
ddl_log_entry.name= *const_cast<LEX_CSTRING*>(table);
ddl_log_entry.tmp_name= *const_cast<LEX_CSTRING*>(trigger_name);
ddl_log_entry.phase= (uchar) phase;
DBUG_RETURN(ddl_log_write(ddl_state, &ddl_log_entry));
}
......@@ -85,6 +85,7 @@ enum ddl_log_action_code
DDL_LOG_CREATE_TABLE_ACTION=12,
DDL_LOG_CREATE_VIEW_ACTION=13,
DDL_LOG_DELETE_TMP_FILE_ACTION=14,
DDL_LOG_CREATE_TRIGGER_ACTION=15,
DDL_LOG_LAST_ACTION /* End marker */
};
......@@ -134,6 +135,13 @@ enum enum_ddl_log_create_view_phase {
DDL_CREATE_VIEW_PHASE_END
};
enum enum_ddl_log_create_trigger_phase {
DDL_CREATE_TRIGGER_PHASE_NO_OLD_TRIGGER,
DDL_CREATE_TRIGGER_PHASE_DELETE_COPY,
DDL_CREATE_TRIGGER_PHASE_OLD_COPIED,
DDL_CREATE_TRIGGER_PHASE_END
};
/*
Setting ddl_log_entry.phase to this has the same effect as setting
......@@ -282,5 +290,9 @@ bool ddl_log_create_view(THD *thd, DDL_LOG_STATE *ddl_state,
bool ddl_log_delete_tmp_file(THD *thd, DDL_LOG_STATE *ddl_state,
const LEX_CSTRING *path,
DDL_LOG_STATE *depending_state);
bool ddl_log_create_trigger(THD *thd, DDL_LOG_STATE *ddl_state,
const LEX_CSTRING *db, const LEX_CSTRING *table,
const LEX_CSTRING *trigger_name,
enum_ddl_log_create_trigger_phase phase);
extern mysql_mutex_t LOCK_gdl;
#endif /* DDL_LOG_INCLUDED */
......@@ -347,6 +347,48 @@ sql_create_definition_file(const LEX_CSTRING *dir,
DBUG_RETURN(TRUE);
}
/*
Make a copy of a definition file with '-' added to the name
@param org_name Original file name
@param new_name Pointer to a buff of FN_REFLEN. Will be updated to name of
backup file
@return 0 ok
@return 1 error
*/
int sql_backup_definition_file(const LEX_CSTRING *org_name,
LEX_CSTRING *new_name)
{
char *new_name_buff= (char*) new_name->str;
new_name->length= org_name->length+1;
memcpy(new_name_buff, org_name->str, org_name->length+1);
new_name_buff[org_name->length]= '-';
new_name_buff[org_name->length+1]= 0;
return my_copy(org_name->str, new_name->str, MYF(MY_WME));
}
/*
Restore copy of a definition file
@param org_name Name of backup file (ending with '-' or '~')
@return 0 ok
@return 1 error
*/
int sql_restore_definition_file(const LEX_CSTRING *name)
{
char new_name[FN_REFLEN+1];
memcpy(new_name, name->str, name->length-1);
new_name[name->length-1]= 0;
return mysql_file_rename(key_file_fileparser, name->str, new_name,
MYF(MY_WME));
}
/**
Renames a frm file (including backups) in same schema.
......
......@@ -96,6 +96,10 @@ my_bool rename_in_schema_file(THD *thd,
const char *schema, const char *old_name,
const char *new_db, const char *new_name);
int sql_backup_definition_file(const LEX_CSTRING *org_name,
LEX_CSTRING *new_name);
int sql_restore_definition_file(const LEX_CSTRING *name);
class File_parser: public Sql_alloc
{
char *start, *end;
......
This diff is collapsed.
......@@ -220,7 +220,9 @@ class Table_triggers_list: public Sql_alloc
}
~Table_triggers_list();
bool create_trigger(THD *thd, TABLE_LIST *table, String *stmt_query);
bool create_trigger(THD *thd, TABLE_LIST *table, String *stmt_query,
DDL_LOG_STATE *ddl_log_state,
DDL_LOG_STATE *ddl_log_state_tmp_file);
bool drop_trigger(THD *thd, TABLE_LIST *table,
LEX_CSTRING *sp_name,
String *stmt_query, DDL_LOG_STATE *ddl_log_state);
......
......@@ -1178,11 +1178,8 @@ static int mysql_register_view(THD *thd, DDL_LOG_STATE *ddl_log_state,
if (old_view_exists)
{
/* Make a backup that we can restore in case of crash */
memcpy(backup_file_name, path.str, path.length);
backup_file_name[path.length]='-';
backup_file_name[path.length+1]= 0;
if (my_copy(path.str, backup_file_name, MYF(MY_WME)))
LEX_CSTRING backup_name= { backup_file_name, 0 };
if (sql_backup_definition_file(&path, &backup_name))
{
error= 1;
goto err;
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment