From a33f9667f6c743d77950a69b8f14c446f4fa8668 Mon Sep 17 00:00:00 2001 From: Tony Chen Date: Thu, 19 Mar 2026 22:00:02 +0000 Subject: [PATCH] [MDEV-36841] Warn when slave timeout drops below heartbeat period When CHANGE MASTER sets master_heartbeat_period to a value greater than @@GLOBAL.slave_net_timeout, a ER_SLAVE_HEARTBEAT_VALUE_OUT_OF_RANGE_MAX warning is emitted. However, the reverse scenario, lowering slave_net_timeout below an existing master_heartbeat_period, produced no warning. Add an on_check callback for slave_net_timeout that iterates all Master_info entries and warns if any heartbeat period exceeds the requested timeout. Connections that are not configured are skipped. A DEFAULT heartbeat period is skipped unless --master-heartbeat-period was given at startup: without it the period follows slave_net_timeout/2 and can never exceed the timeout, but with it the period is a fixed value and is checked like an explicit one. The warning is reported with a new, generalized error ER_OPERATION_INVALIDATES_CONFIGURED_SLAVE, which names the affected replication connection and carries a parameterized description of the misconfiguration. Naming the connection matters for multi-source: when several connections exceed the new timeout, the user can see exactly which ones are problematic. One warning is emitted per affected connection. Add a multi_source test, run with --master-heartbeat-period=30, that configures two named connections with explicit heartbeat periods above the timeout and a third left at DEFAULT, and verifies a distinct warning is emitted for each. All new code of the whole pull request, including one or several files that are either new files or modified ones, are contributed under the BSD-new license. I am contributing on behalf of my employer Amazon Web Services, Inc. --- .../binlog_in_engine/rpl_heartbeat.result | 2 + .../slave_net_timeout_heartbeat.opt | 1 + .../slave_net_timeout_heartbeat.result | 17 +++++++ .../slave_net_timeout_heartbeat.test | 48 +++++++++++++++++++ mysql-test/suite/rpl/r/rpl_heartbeat.result | 2 + .../suite/rpl/r/rpl_heartbeat_basic.result | 11 ++++- .../suite/rpl/t/rpl_heartbeat_basic.test | 14 +++++- sql/share/errmsg-utf8.txt | 2 + sql/sys_vars.cc | 46 +++++++++++++++++- 9 files changed, 139 insertions(+), 4 deletions(-) create mode 100644 mysql-test/suite/multi_source/slave_net_timeout_heartbeat.opt create mode 100644 mysql-test/suite/multi_source/slave_net_timeout_heartbeat.result create mode 100644 mysql-test/suite/multi_source/slave_net_timeout_heartbeat.test diff --git a/mysql-test/suite/binlog_in_engine/rpl_heartbeat.result b/mysql-test/suite/binlog_in_engine/rpl_heartbeat.result index e60e753aa14c1..1c886cdb0b494 100644 --- a/mysql-test/suite/binlog_in_engine/rpl_heartbeat.result +++ b/mysql-test/suite/binlog_in_engine/rpl_heartbeat.result @@ -14,6 +14,8 @@ show status like 'Slave_heartbeat_period';; Variable_name Slave_heartbeat_period Value 4.000 set @@global.slave_net_timeout= 3 /* must be a warning */; +Warnings: +Warning 4276 This operation leaves replication connection '' with an invalid configuration: the heartbeat period (4.000 seconds) exceeds the requested slave_net_timeout (3 seconds); a sensible value for the period should be less than the timeout reset slave; connection master; drop table if exists t1; diff --git a/mysql-test/suite/multi_source/slave_net_timeout_heartbeat.opt b/mysql-test/suite/multi_source/slave_net_timeout_heartbeat.opt new file mode 100644 index 0000000000000..2c46053dd9b57 --- /dev/null +++ b/mysql-test/suite/multi_source/slave_net_timeout_heartbeat.opt @@ -0,0 +1 @@ +--master-heartbeat-period=30 diff --git a/mysql-test/suite/multi_source/slave_net_timeout_heartbeat.result b/mysql-test/suite/multi_source/slave_net_timeout_heartbeat.result new file mode 100644 index 0000000000000..944e653c05c24 --- /dev/null +++ b/mysql-test/suite/multi_source/slave_net_timeout_heartbeat.result @@ -0,0 +1,17 @@ +connect slave,127.0.0.1,root,,,$SERVER_MYPORT_3; +connection slave; +SET @restore_slave_net_timeout= @@global.slave_net_timeout; +SET @@global.slave_net_timeout= 60; +change master 'slave1' to master_port=MYPORT_1, master_host='127.0.0.1', master_user='root', master_ssl_verify_server_cert=0, master_heartbeat_period=40; +change master 'slave2' to master_port=MYPORT_2, master_host='127.0.0.1', master_user='root', master_ssl_verify_server_cert=0, master_heartbeat_period=50; +change master 'slave3' to master_port=MYPORT_1, master_host='localhost', master_user='root', master_ssl_verify_server_cert=0; +SET @@global.slave_net_timeout= 20; +SHOW WARNINGS; +Level Code Message +Warning 4276 This operation leaves replication connection 'slave1' with an invalid configuration: the heartbeat period (40.000 seconds) exceeds the requested slave_net_timeout (20 seconds); a sensible value for the period should be less than the timeout +Warning 4276 This operation leaves replication connection 'slave2' with an invalid configuration: the heartbeat period (50.000 seconds) exceeds the requested slave_net_timeout (20 seconds); a sensible value for the period should be less than the timeout +Warning 4276 This operation leaves replication connection 'slave3' with an invalid configuration: the heartbeat period (30.000 seconds) exceeds the requested slave_net_timeout (20 seconds); a sensible value for the period should be less than the timeout +SET @@global.slave_net_timeout= @restore_slave_net_timeout; +include/reset_master_slave.inc +disconnect slave; +connection default; diff --git a/mysql-test/suite/multi_source/slave_net_timeout_heartbeat.test b/mysql-test/suite/multi_source/slave_net_timeout_heartbeat.test new file mode 100644 index 0000000000000..7a7b592fe8e24 --- /dev/null +++ b/mysql-test/suite/multi_source/slave_net_timeout_heartbeat.test @@ -0,0 +1,48 @@ +# +# MDEV-36841: Lowering @@global.slave_net_timeout below an explicitly +# configured master_heartbeat_period warns once per replication connection. +# +# With multi-source replication there can be more than one connection whose +# heartbeat period exceeds the new timeout, so the warning names each affected +# connection. This test configures two named connections, each with an +# explicit heartbeat period above the timeout we set next, and verifies that a +# separate warning is emitted for both. +# +# The server is started with --master-heartbeat-period=30, so a third +# connection left at the DEFAULT heartbeat period resolves to a fixed 30 +# seconds rather than slave_net_timeout/2, and must warn as well. +# +--source include/not_embedded.inc + +--connect (slave,127.0.0.1,root,,,$SERVER_MYPORT_3) + +--connection slave +SET @restore_slave_net_timeout= @@global.slave_net_timeout; +SET @@global.slave_net_timeout= 60; + +# Two sources, each with an explicit heartbeat period below the current +# timeout (60), so configuring them does not warn. +--replace_result $SERVER_MYPORT_1 MYPORT_1 +eval change master 'slave1' to master_port=$SERVER_MYPORT_1, master_host='127.0.0.1', master_user='root', master_ssl_verify_server_cert=0, master_heartbeat_period=40; +--replace_result $SERVER_MYPORT_2 MYPORT_2 +eval change master 'slave2' to master_port=$SERVER_MYPORT_2, master_host='127.0.0.1', master_user='root', master_ssl_verify_server_cert=0, master_heartbeat_period=50; +# DEFAULT heartbeat period, i.e. --master-heartbeat-period=30. The +# connection is never started; 'localhost' only avoids the host:port +# conflict with 'slave1'. +--replace_result $SERVER_MYPORT_1 MYPORT_1 +eval change master 'slave3' to master_port=$SERVER_MYPORT_1, master_host='localhost', master_user='root', master_ssl_verify_server_cert=0; + +# Lowering the timeout below all three heartbeat periods must warn once per +# connection. Warnings are pushed in master_info hash order, which is not +# deterministic, so capture them with a sorted SHOW WARNINGS. +--disable_warnings +SET @@global.slave_net_timeout= 20; +--enable_warnings +--sorted_result +SHOW WARNINGS; + +# Clean up +SET @@global.slave_net_timeout= @restore_slave_net_timeout; +--source include/reset_master_slave.inc +--disconnect slave +--connection default diff --git a/mysql-test/suite/rpl/r/rpl_heartbeat.result b/mysql-test/suite/rpl/r/rpl_heartbeat.result index 01f82e2ad3085..7c0b9d7b29a36 100644 --- a/mysql-test/suite/rpl/r/rpl_heartbeat.result +++ b/mysql-test/suite/rpl/r/rpl_heartbeat.result @@ -14,6 +14,8 @@ show status like 'Slave_heartbeat_period';; Variable_name Slave_heartbeat_period Value 4.000 set @@global.slave_net_timeout= 3 /* must be a warning */; +Warnings: +Warning 4276 This operation leaves replication connection '' with an invalid configuration: the heartbeat period (4.000 seconds) exceeds the requested slave_net_timeout (3 seconds); a sensible value for the period should be less than the timeout reset slave; connection master; drop table if exists t1; diff --git a/mysql-test/suite/rpl/r/rpl_heartbeat_basic.result b/mysql-test/suite/rpl/r/rpl_heartbeat_basic.result index 463503534c496..ca06836cc783c 100644 --- a/mysql-test/suite/rpl/r/rpl_heartbeat_basic.result +++ b/mysql-test/suite/rpl/r/rpl_heartbeat_basic.result @@ -54,11 +54,20 @@ Slave_heartbeat_period 25.000 SET @@global.slave_net_timeout=@restore_slave_net_timeout; include/reset_slave.inc -*** Warning if updated slave_net_timeout < slave_heartbeat_timeout *** +*** No warning if slave_net_timeout lowered with default heartbeat_period *** SET @@global.slave_net_timeout=FLOOR(SLAVE_HEARTBEAT_TIMEOUT)-1; SET @@global.slave_net_timeout=@restore_slave_net_timeout; RESET SLAVE; +*** Warning if slave_net_timeout set below explicit heartbeat_period *** +SET @@global.slave_net_timeout=60; +CHANGE MASTER TO MASTER_HOST='127.0.0.1', MASTER_PORT=MASTER_PORT, MASTER_USER='root', MASTER_CONNECT_RETRY=20, MASTER_HEARTBEAT_PERIOD=40; +SET @@global.slave_net_timeout=20; +Warnings: +Warning 4276 This operation leaves replication connection '' with an invalid configuration: the heartbeat period (40.000 seconds) exceeds the requested slave_net_timeout (20 seconds); a sensible value for the period should be less than the timeout +SET @@global.slave_net_timeout=@restore_slave_net_timeout; +RESET SLAVE; + *** Warning if updated slave_heartbeat_timeout > slave_net_timeout *** CHANGE MASTER TO MASTER_HOST='127.0.0.1', MASTER_PORT=MASTER_PORT, MASTER_USER='root', MASTER_CONNECT_RETRY=20, MASTER_HEARTBEAT_PERIOD=SLAVE_NET_TIMEOUT; Warnings: diff --git a/mysql-test/suite/rpl/t/rpl_heartbeat_basic.test b/mysql-test/suite/rpl/t/rpl_heartbeat_basic.test index 43e065bb9051f..3cb79b8b9cb7b 100644 --- a/mysql-test/suite/rpl/t/rpl_heartbeat_basic.test +++ b/mysql-test/suite/rpl/t/rpl_heartbeat_basic.test @@ -104,8 +104,8 @@ SET @@global.slave_net_timeout=@restore_slave_net_timeout; --source include/reset_slave.inc --echo -# Set slave_net_timeout less than current value of slave_heartbeat_period ---echo *** Warning if updated slave_net_timeout < slave_heartbeat_timeout *** +# No warning when heartbeat_period is DEFAULT (automatically slave_net_timeout/2) +--echo *** No warning if slave_net_timeout lowered with default heartbeat_period *** let $slave_heartbeat_timeout= query_get_value(SHOW GLOBAL STATUS LIKE 'slave_heartbeat_period', Value, 1); --replace_result $slave_heartbeat_timeout SLAVE_HEARTBEAT_TIMEOUT eval SET @@global.slave_net_timeout=FLOOR($slave_heartbeat_timeout)-1; @@ -113,6 +113,16 @@ SET @@global.slave_net_timeout=@restore_slave_net_timeout; RESET SLAVE; --echo +# Set slave_net_timeout less than explicitly set slave_heartbeat_period (MDEV-36841) +--echo *** Warning if slave_net_timeout set below explicit heartbeat_period *** +SET @@global.slave_net_timeout=60; +--replace_result $MASTER_MYPORT MASTER_PORT +eval CHANGE MASTER TO MASTER_HOST='127.0.0.1', MASTER_PORT=$MASTER_MYPORT, MASTER_USER='root', MASTER_CONNECT_RETRY=$connect_retry, MASTER_HEARTBEAT_PERIOD=40; +SET @@global.slave_net_timeout=20; +SET @@global.slave_net_timeout=@restore_slave_net_timeout; +RESET SLAVE; +--echo + # Set value of slave_heartbeat_period greater than slave_net_timeout --echo *** Warning if updated slave_heartbeat_timeout > slave_net_timeout *** let $slave_net_timeout= query_get_value(SHOW VARIABLES LIKE 'slave_net_timeout', Value, 1); diff --git a/sql/share/errmsg-utf8.txt b/sql/share/errmsg-utf8.txt index 3bc85fefad84b..fa8a4215eb843 100644 --- a/sql/share/errmsg-utf8.txt +++ b/sql/share/errmsg-utf8.txt @@ -12427,3 +12427,5 @@ ER_INVALID_HOST_NETMASK eng "Invalid masked host '%s' for user '%s'" ER_ACL_ENTRY_IGNORED_BAD_HOST_MASK eng "'%s' entry '%s' ignored, the host is not a valid ip/netmask" +ER_OPERATION_INVALIDATES_CONFIGURED_SLAVE + eng "This operation leaves replication connection '%s' with an invalid configuration: %s" diff --git a/sql/sys_vars.cc b/sql/sys_vars.cc index 08251d6dd2883..454d49ebb9149 100644 --- a/sql/sys_vars.cc +++ b/sql/sys_vars.cc @@ -6151,13 +6151,57 @@ static Sys_var_uint Sys_opt_binlog_partial_rows_event_max_size( CMD_LINE(REQUIRED_ARG), VALID_RANGE(1024, MAX_MAX_ALLOWED_PACKET), DEFAULT(MAX_MAX_ALLOWED_PACKET), BLOCK_SIZE(1024)); +static bool check_slave_net_timeout(sys_var *self, THD *thd, set_var *var) +{ + uint timeout= (uint) var->save_result.ulonglong_value; + mysql_mutex_lock(&LOCK_active_mi); + if (master_info_index) + { + for (uint i= 0; i < master_info_index->master_info_hash.records; i++) + { + Master_info *mi= (Master_info *) + my_hash_element(&master_info_index->master_info_hash, i); + /* + Skip connections that were never configured (e.g. the implicit + default connection), as START SLAVE does. + A DEFAULTed heartbeat period without --master-heartbeat-period + follows slave_net_timeout/2 and so can never exceed the new timeout. + get_value() would still report half of the old timeout, so skip it. + */ + if (!*mi->host || + (mi->master_heartbeat_period.is_default() && + master_heartbeat_period_is_auto)) + continue; + uint32 period= mi->master_heartbeat_period.get_value(); + if (period <= timeout * 1000ULL) + continue; + + const char *name= mi->connection_name.str ? mi->connection_name.str : ""; + char detail[256]; + my_snprintf(detail, sizeof(detail), + "the heartbeat period (%.3f seconds) exceeds the requested " + "slave_net_timeout (%u seconds); a sensible value for the " + "period should be less than the timeout", + period / 1000.0, + timeout); + push_warning_printf(thd, Sql_condition::WARN_LEVEL_WARN, + ER_OPERATION_INVALIDATES_CONFIGURED_SLAVE, + ER_THD(thd, ER_OPERATION_INVALIDATES_CONFIGURED_SLAVE), + name, detail); + } + } + mysql_mutex_unlock(&LOCK_active_mi); + return false; +} + static Sys_var_on_access_global Sys_slave_net_timeout( "slave_net_timeout", "Number of seconds to wait for more data " "from any master/slave connection before aborting the read", GLOBAL_VAR(slave_net_timeout), CMD_LINE(REQUIRED_ARG), - VALID_RANGE(1, LONG_TIMEOUT), DEFAULT(SLAVE_NET_TIMEOUT), BLOCK_SIZE(1)); + VALID_RANGE(1, LONG_TIMEOUT), DEFAULT(SLAVE_NET_TIMEOUT), BLOCK_SIZE(1), + NO_MUTEX_GUARD, NOT_IN_BINLOG, ON_CHECK(check_slave_net_timeout)); static Sys_var_uint Sys_master_connect_retry(