From 5a330df7b51ff10a296d87021a1d886dc6c63a96 Mon Sep 17 00:00:00 2001 From: Mahmoud Khaled Date: Tue, 12 May 2026 16:56:31 +0300 Subject: [PATCH] MDEV-7394: Make slave_skip_errors dynamic Allow slave_skip_errors to be changed at runtime when the slave is stopped. Previously it was read-only. Split init_slave_skip_errors() into a bitmap-init phase (init_slave_skip_errors_bitmap()) and the existing make_slave_skip_errors_printable(). Added check function check_slave_skip_errors() that rejects changes while the slave is running, and update function update_slave_skip_errors() that rebuilds the bitmap and printable string on SET GLOBAL. The update callback calls self->cleanup() between the two phases to free the heap string allocated by global_update() before the pointer is overwritten with the static slave_skip_error_names[] buffer. Added test rpl.rpl_slave_skip_errors_writable. Reviewed-by: Brandon Nesterenko Reviewed-by: Georgi Kodinov --- mysql-test/main/variables-notembedded.result | 11 ++- mysql-test/main/variables-notembedded.test | 9 ++- .../rpl/r/slave_skip_errors_writable.result | 62 +++++++++++++++++ .../rpl/t/slave_skip_errors_writable.test | 69 +++++++++++++++++++ .../sys_vars/r/slave_skip_errors_basic.result | 4 -- .../r/sysvars_server_notembedded.result | 2 +- .../sys_vars/t/slave_skip_errors_basic.test | 7 -- sql/slave.cc | 52 +++++++++----- sql/slave.h | 4 ++ sql/sys_vars.cc | 49 ++++++++++++- 10 files changed, 230 insertions(+), 39 deletions(-) create mode 100644 mysql-test/suite/rpl/r/slave_skip_errors_writable.result create mode 100644 mysql-test/suite/rpl/t/slave_skip_errors_writable.test diff --git a/mysql-test/main/variables-notembedded.result b/mysql-test/main/variables-notembedded.result index 05c87314b05a8..36a666fb2cd51 100644 --- a/mysql-test/main/variables-notembedded.result +++ b/mysql-test/main/variables-notembedded.result @@ -1,5 +1,6 @@ ---- Init ---- set @my_slave_net_timeout =@@global.slave_net_timeout; +set @my_slave_skip_errors =@@global.slave_skip_errors; ---- Test ---- set global slave_net_timeout=100; set global sql_slave_skip_counter=100; @@ -144,9 +145,13 @@ SELECT @@global.slave_skip_errors; @@global.slave_skip_errors 0,3,100,137,643,1752 SET @@session.slave_skip_errors= 7; -ERROR HY000: Variable 'slave_skip_errors' is a read only variable -SET @@global.slave_skip_errors= 7; -ERROR HY000: Variable 'slave_skip_errors' is a read only variable +ERROR HY000: Variable 'slave_skip_errors' is a GLOBAL variable and should be set with SET GLOBAL +SET @@global.slave_skip_errors= "7"; +SELECT @@global.slave_skip_errors; +@@global.slave_skip_errors +7 +SET @@global.slave_skip_errors= "OFF"; +set GLOBAL slave_skip_errors = @my_slave_skip_errors; # # Bug #11766769 : 59959: SMALL VALUES OF --MAX-ALLOWED-PACKET # ARE NOT BEING HONORED diff --git a/mysql-test/main/variables-notembedded.test b/mysql-test/main/variables-notembedded.test index 6c08f5357d03a..30f1e0e3cc81b 100644 --- a/mysql-test/main/variables-notembedded.test +++ b/mysql-test/main/variables-notembedded.test @@ -7,6 +7,7 @@ source include/not_embedded.inc; --echo ---- Init ---- # Backup global variables so they can be restored at end of test. set @my_slave_net_timeout =@@global.slave_net_timeout; +set @my_slave_skip_errors =@@global.slave_skip_errors; --echo ---- Test ---- set global slave_net_timeout=100; @@ -145,10 +146,12 @@ SHOW VARIABLES like 'slave_skip_errors'; --error ER_INCORRECT_GLOBAL_LOCAL_VAR SELECT @@session.slave_skip_errors; SELECT @@global.slave_skip_errors; ---error ER_INCORRECT_GLOBAL_LOCAL_VAR +--error ER_GLOBAL_VARIABLE SET @@session.slave_skip_errors= 7; ---error ER_INCORRECT_GLOBAL_LOCAL_VAR -SET @@global.slave_skip_errors= 7; +SET @@global.slave_skip_errors= "7"; +SELECT @@global.slave_skip_errors; +SET @@global.slave_skip_errors= "OFF"; +set GLOBAL slave_skip_errors = @my_slave_skip_errors; # --echo # diff --git a/mysql-test/suite/rpl/r/slave_skip_errors_writable.result b/mysql-test/suite/rpl/r/slave_skip_errors_writable.result new file mode 100644 index 0000000000000..82b815be8bdb5 --- /dev/null +++ b/mysql-test/suite/rpl/r/slave_skip_errors_writable.result @@ -0,0 +1,62 @@ +# MDEV-7394 test dynamic slave_skip_errors (writable when slaves stopped) +include/master-slave.inc +[connection master] +connection slave; +STOP SLAVE; +SET GLOBAL slave_skip_errors = "1062"; +START SLAVE; +SELECT @@global.slave_skip_errors; +@@global.slave_skip_errors +1062 +connection master; +CREATE TABLE t1 (id INT PRIMARY KEY); +include/sync_slave_sql_with_master.inc +connection slave; +INSERT INTO t1 VALUES (2); +connection master; +INSERT INTO t1 VALUES (2); +include/sync_slave_sql_with_master.inc +connection slave; +include/check_slave_is_running.inc +START SLAVE; +Warnings: +Note 1254 Slave is already running +include/wait_for_slave_to_start.inc +SET GLOBAL slave_skip_errors = "1062"; +ERROR HY000: This operation cannot be performed as you have a running slave ''; run STOP SLAVE '' first +STOP SLAVE; +SET GLOBAL slave_skip_errors = "1050,1004,1104,1146"; +START SLAVE; +# erros should be showed in sorted order +SELECT @@global.slave_skip_errors; +@@global.slave_skip_errors +1004,1050,1104,1146 +SET GLOBAL slave_ddl_exec_mode = STRICT; +# test that mulitiple errors are set correctly +connection slave; +include/stop_slave.inc +CREATE TABLE t2 (id INT); +include/start_slave.inc +connection master; +CREATE TABLE t2 (id INT); +INSERT INTO t1 VALUES (4); +connection slave; +connection slave; +include/check_slave_is_running.inc +connection master; +INSERT INTO t1 VALUES (3); +include/sync_slave_sql_with_master.inc +connection slave; +SELECT COUNT(*) FROM t1 WHERE id=3; +COUNT(*) +1 +STOP SLAVE; +SET GLOBAL slave_skip_errors = "OFF"; +START SLAVE; +connection master; +DROP TABLE t1; +DROP TABLE t2; +include/sync_slave_sql_with_master.inc +connection slave; +SET GLOBAL slave_ddl_exec_mode = DEFAULT; +include/rpl_end.inc diff --git a/mysql-test/suite/rpl/t/slave_skip_errors_writable.test b/mysql-test/suite/rpl/t/slave_skip_errors_writable.test new file mode 100644 index 0000000000000..a62bea2ae7f94 --- /dev/null +++ b/mysql-test/suite/rpl/t/slave_skip_errors_writable.test @@ -0,0 +1,69 @@ +--echo # MDEV-7394 test dynamic slave_skip_errors (writable when slaves stopped) + +--source include/master-slave.inc +connection slave; +STOP SLAVE; +SET GLOBAL slave_skip_errors = "1062"; +START SLAVE; +SELECT @@global.slave_skip_errors; + +connection master; +CREATE TABLE t1 (id INT PRIMARY KEY); +--source include/sync_slave_sql_with_master.inc + +connection slave; +INSERT INTO t1 VALUES (2); + +connection master; +INSERT INTO t1 VALUES (2); +--source include/sync_slave_sql_with_master.inc + +connection slave; +source include/check_slave_is_running.inc; +START SLAVE; +--source include/wait_for_slave_to_start.inc +--error ER_SLAVE_MUST_STOP +SET GLOBAL slave_skip_errors = "1062"; + +STOP SLAVE; +SET GLOBAL slave_skip_errors = "1050,1004,1104,1146"; +START SLAVE; + +--echo # erros should be showed in sorted order +SELECT @@global.slave_skip_errors; + +SET GLOBAL slave_ddl_exec_mode = STRICT; +--echo # test that mulitiple errors are set correctly + +connection slave; +--source include/stop_slave.inc +CREATE TABLE t2 (id INT); +--source include/start_slave.inc + +connection master; +CREATE TABLE t2 (id INT); +INSERT INTO t1 VALUES (4); +sync_slave_with_master; + +connection slave; +source include/check_slave_is_running.inc; + +connection master; +INSERT INTO t1 VALUES (3); +--source include/sync_slave_sql_with_master.inc + +connection slave; +SELECT COUNT(*) FROM t1 WHERE id=3; + +STOP SLAVE; +SET GLOBAL slave_skip_errors = "OFF"; +START SLAVE; + +connection master; +DROP TABLE t1; +DROP TABLE t2; +--source include/sync_slave_sql_with_master.inc +connection slave; +SET GLOBAL slave_ddl_exec_mode = DEFAULT; + +--source include/rpl_end.inc diff --git a/mysql-test/suite/sys_vars/r/slave_skip_errors_basic.result b/mysql-test/suite/sys_vars/r/slave_skip_errors_basic.result index b74c7d3847a66..906c357a74452 100644 --- a/mysql-test/suite/sys_vars/r/slave_skip_errors_basic.result +++ b/mysql-test/suite/sys_vars/r/slave_skip_errors_basic.result @@ -15,7 +15,3 @@ SLAVE_SKIP_ERRORS OFF select * from information_schema.session_variables where variable_name='slave_skip_errors'; VARIABLE_NAME VARIABLE_VALUE SLAVE_SKIP_ERRORS OFF -set global slave_skip_errors=1; -ERROR HY000: Variable 'slave_skip_errors' is a read only variable -set session slave_skip_errors=1; -ERROR HY000: Variable 'slave_skip_errors' is a read only variable diff --git a/mysql-test/suite/sys_vars/r/sysvars_server_notembedded.result b/mysql-test/suite/sys_vars/r/sysvars_server_notembedded.result index 268c06632b8b4..635f5c1d0b91b 100644 --- a/mysql-test/suite/sys_vars/r/sysvars_server_notembedded.result +++ b/mysql-test/suite/sys_vars/r/sysvars_server_notembedded.result @@ -4350,7 +4350,7 @@ NUMERIC_MIN_VALUE NULL NUMERIC_MAX_VALUE NULL NUMERIC_BLOCK_SIZE NULL ENUM_VALUE_LIST NULL -READ_ONLY YES +READ_ONLY NO COMMAND_LINE_ARGUMENT REQUIRED VARIABLE_NAME SLAVE_SQL_VERIFY_CHECKSUM VARIABLE_SCOPE GLOBAL diff --git a/mysql-test/suite/sys_vars/t/slave_skip_errors_basic.test b/mysql-test/suite/sys_vars/t/slave_skip_errors_basic.test index c5c8aba7e3a0a..840f72391d40c 100644 --- a/mysql-test/suite/sys_vars/t/slave_skip_errors_basic.test +++ b/mysql-test/suite/sys_vars/t/slave_skip_errors_basic.test @@ -10,10 +10,3 @@ show session variables like 'slave_skip_errors'; select * from information_schema.global_variables where variable_name='slave_skip_errors'; select * from information_schema.session_variables where variable_name='slave_skip_errors'; -# -# show that it's read-only -# ---error ER_INCORRECT_GLOBAL_LOCAL_VAR -set global slave_skip_errors=1; ---error ER_INCORRECT_GLOBAL_LOCAL_VAR -set session slave_skip_errors=1; diff --git a/sql/slave.cc b/sql/slave.cc index 82d265ab327a8..45dcbfd68a1d9 100644 --- a/sql/slave.cc +++ b/sql/slave.cc @@ -152,7 +152,6 @@ static bool wait_for_relay_log_space(Relay_log_info* rli); static bool io_slave_killed(Master_info* mi); static bool sql_slave_killed(rpl_group_info *rgi); static int init_slave_thread(THD*, Master_info *, SLAVE_THD_TYPE); -static void make_slave_skip_errors_printable(void); static void make_slave_transaction_retry_errors_printable(void); static int safe_connect(THD* thd, MYSQL* mysql, Master_info* mi); static int safe_reconnect(THD*, MYSQL*, Master_info*, bool); @@ -809,7 +808,7 @@ int init_recovery(Master_info* mi, const char** errmsg) Convert slave skip errors bitmap into a printable string. */ -static void make_slave_skip_errors_printable(void) +void make_slave_skip_errors_printable(void) { /* To be safe, we want 10 characters of room in the buffer for a number @@ -829,12 +828,14 @@ static void make_slave_skip_errors_printable(void) { /* purecov: begin tested */ memcpy(slave_skip_error_names, STRING_WITH_LEN("OFF")); + slave_skip_error_names[3]= '\0'; /* purecov: end */ } else if (bitmap_is_set_all(&slave_error_mask)) { /* purecov: begin tested */ memcpy(slave_skip_error_names, STRING_WITH_LEN("ALL")); + slave_skip_error_names[3]= '\0'; /* purecov: end */ } else @@ -866,18 +867,7 @@ static void make_slave_skip_errors_printable(void) DBUG_VOID_RETURN; } -/* - Init function to set up array for errors that should be skipped for slave - - SYNOPSIS - init_slave_skip_errors() - arg List of errors numbers to skip, separated with ',' - - NOTES - Called from get_options() in mysqld.cc on start-up -*/ - -bool init_slave_skip_errors(const char* arg) +bool init_slave_skip_errors_bitmap(const char* arg) { const char *p; DBUG_ENTER("init_slave_skip_errors"); @@ -885,14 +875,20 @@ bool init_slave_skip_errors(const char* arg) if (!arg || !*arg) // No errors defined goto end; - if (my_bitmap_init(&slave_error_mask,0,MAX_SLAVE_ERROR)) - DBUG_RETURN(1); + if (!use_slave_mask) + { + if (my_bitmap_init(&slave_error_mask,0,MAX_SLAVE_ERROR)) + DBUG_RETURN(1); + } + else + { + bitmap_clear_all(&slave_error_mask); + } use_slave_mask= 1; for (;my_isspace(system_charset_info,*arg);++arg) /* empty */; - if (!system_charset_info->strnncoll((uchar*)arg,4,(const uchar*)"all",4)) - { + if (strlen(arg) == 3 && !system_charset_info->strnncoll((uchar*)arg,3,(const uchar*)"all",3)) { bitmap_set_all(&slave_error_mask); bitmap_clear_bit(&slave_error_mask,ER_CONNECTION_KILLED); sql_print_warning("Slave: ER_CONNECTION_KILLED (%d) is not allowed in " @@ -923,10 +919,28 @@ bool init_slave_skip_errors(const char* arg) } end: - make_slave_skip_errors_printable(); DBUG_RETURN(0); } +/* + Init function to set up array for errors that should be skipped for slave + + SYNOPSIS + init_slave_skip_errors() + arg List of errors numbers to skip, separated with ',' + NOTES + Called from get_options() in mysqld.cc on start-up +*/ +bool init_slave_skip_errors(const char* arg) +{ + DBUG_ENTER("init_slave_skip_errors"); + if (init_slave_skip_errors_bitmap(arg)) + { + DBUG_RETURN(1); + } + make_slave_skip_errors_printable(); + DBUG_RETURN(0); +} /** Make printable version if slave_transaction_retry_errors This is never empty as at least ER_LOCK_DEADLOCK and ER_LOCK_WAIT_TIMEOUT diff --git a/sql/slave.h b/sql/slave.h index 6b22048a226f0..8df877be24e06 100644 --- a/sql/slave.h +++ b/sql/slave.h @@ -174,6 +174,10 @@ extern const char *relay_log_basename; int init_slave(); int init_recovery(Master_info* mi, const char** errmsg); bool init_slave_skip_errors(const char* arg); +bool init_slave_skip_errors_bitmap(const char* arg); +void make_slave_skip_errors_printable(void); +bool check_slave_skip_errors(sys_var *self, THD *thd, set_var *var); +bool update_slave_skip_errors(sys_var *self, THD *thd, enum_var_type type); bool init_slave_transaction_retry_errors(const char* arg); int register_slave_on_master(MYSQL* mysql); int terminate_slave_threads(Master_info* mi, int thread_mask, diff --git a/sql/sys_vars.cc b/sql/sys_vars.cc index 3e0f1fa1cb795..bbd7a468456c7 100644 --- a/sql/sys_vars.cc +++ b/sql/sys_vars.cc @@ -6212,12 +6212,57 @@ static Sys_var_multi_source_ulonglong Sys_max_relay_log_size( VALID_RANGE(0, 1024L*1024*1024), DEFAULT(0), BLOCK_SIZE(IO_SIZE), ON_UPDATE(update_max_relay_log_size)); +bool check_slave_skip_errors(sys_var *self, THD *thd, set_var *var) +{ + return give_error_if_slave_running(0); +} + +bool update_slave_skip_errors(sys_var *self, THD *thd, enum_var_type type) +{ + bool err= false; + mysql_mutex_unlock(&LOCK_global_system_variables); + mysql_mutex_lock(&LOCK_active_mi); + + if (give_error_if_slave_running(1)) + { + my_error(ER_SLAVE_MUST_STOP, MYF(0)); + err= true; + } + if (!err) + { + if (init_slave_skip_errors_bitmap(opt_slave_skip_errors)) + { + my_error(ER_OUT_OF_RESOURCES, MYF(0)); + err= true; + } + else + { + /* + * global_update() already ran my_memdup() on the user's string and stored + * the heap pointer in opt_slave_skip_errors with ALLOCATED set. + * We must call self->cleanup() after building the bitmap to free that + * heap allocation before make_slave_skip_errors_printable() overwrites + * opt_slave_skip_errors with the static slave_skip_error_names[] buffer. + * Without this, the heap pointer is lost (leak) and ALLOCATED stays set, + * causing a crash when cleanup() later tries to free the static buffer. + */ + self->cleanup(); + make_slave_skip_errors_printable(); + } + } + + mysql_mutex_unlock(&LOCK_active_mi); + mysql_mutex_lock(&LOCK_global_system_variables); + return err; +} static Sys_var_charptr Sys_slave_skip_errors( "slave_skip_errors", "Tells the slave thread to continue " "replication when a query event returns an error from the " "provided list", - READ_ONLY GLOBAL_VAR(opt_slave_skip_errors), CMD_LINE(REQUIRED_ARG), - DEFAULT(0)); + GLOBAL_VAR(opt_slave_skip_errors), CMD_LINE(REQUIRED_ARG), + DEFAULT(NULL), NO_MUTEX_GUARD, NOT_IN_BINLOG, + ON_CHECK(check_slave_skip_errors), + ON_UPDATE(update_slave_skip_errors)); static Sys_var_on_access_global