From 0c1bf5e247e86260359852c0d3753cd53b8ae937 Mon Sep 17 00:00:00 2001 From: Brandon Nesterenko Date: Fri, 4 Aug 2023 15:48:13 -0600 Subject: [PATCH] MDEV-27247: Add keywords "SQL_BEFORE_GTIDS" and "SQL_AFTER_GTIDS" for START SLAVE UNTIL MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New Feature: ============ This patch extends the START SLAVE UNTIL command with options SQL_BEFORE_GTIDS and SQL_AFTER_GTIDS to allow user control of whether the replica stops before or after a provided GTID state. Its syntax is: START SLAVE UNTIL (SQL_BEFORE_GTIDS|SQL_AFTER_GTIDS)=”” When providing SQL_BEFORE_GTIDS=””, for each domain specified in the gtid_list, the replica will execute transactions up to the GTID found, and immediately stop processing events in that domain (without executing the transaction of the specified GTID). Once all domains have stopped, the replica will stop. Events originating from domains that are not specified in the list are not replicated. START SLAVE UNTIL SQL_AFTER_GTIDS=”” is an alias to the default behavior of START SLAVE UNTIL master_gtid_pos=””. That is, the replica will only execute transactions originating from domain ids provided in the list, and will stop once all transactions provided in the UNTIL list have all been executed. Example: ========= If a primary server has a binary log consisting of the following GTIDs: 0-1-1 1-1-1 0-1-2 1-1-2 0-1-3 1-1-3 If a fresh replica (i.e. one with an empty GTID position, @@gtid_slave_pos='') is started with SQL_BEFORE_GTIDS, i.e. START SLAVE UNTIL SQL_BEFORE_GTIDS=”1-1-2” The resulting gtid_slave_pos of the replica will be “1-1-1”. This is because the replica will execute only events from domain 1 until it sees the transaction with sequence number 2, and immediately stop without executing it. If the replica is started with SQL_AFTER_GTIDS, i.e. START SLAVE UNTIL SQL_AFTER_GTIDS=”1-1-2” then the resulting gtid_slave_pos of the replica will be “1-1-2”. This is because it will only execute events from domain 1 until it has executed the provided GTID. Reviewed By: ============ Kristian Nielson --- .../suite/perfschema/r/digest_view.result | 50 +-- .../start_server_low_digest_sql_length.result | 4 +- .../rpl_gtid_until_before_after_gtids.test | 375 ++++++++++++++++++ .../rpl_gtid_until_before_after_gtids.result | 239 +++++++++++ .../t/rpl_gtid_until_before_after_gtids.test | 45 +++ sql/lex.h | 2 + sql/rpl_rli.cc | 3 +- sql/rpl_rli.h | 2 + sql/slave.cc | 36 ++ sql/sql_lex.h | 2 + sql/sql_repl.cc | 35 +- sql/sql_yacc.yy | 13 + 12 files changed, 772 insertions(+), 34 deletions(-) create mode 100644 mysql-test/suite/rpl/include/rpl_gtid_until_before_after_gtids.test create mode 100644 mysql-test/suite/rpl/r/rpl_gtid_until_before_after_gtids.result create mode 100644 mysql-test/suite/rpl/t/rpl_gtid_until_before_after_gtids.test diff --git a/mysql-test/suite/perfschema/r/digest_view.result b/mysql-test/suite/perfschema/r/digest_view.result index 9b9f31dcde9..e6913994177 100644 --- a/mysql-test/suite/perfschema/r/digest_view.result +++ b/mysql-test/suite/perfschema/r/digest_view.result @@ -191,17 +191,17 @@ SELECT SCHEMA_NAME, DIGEST, DIGEST_TEXT, COUNT_STAR FROM performance_schema.events_statements_summary_by_digest ORDER BY DIGEST_TEXT; SCHEMA_NAME DIGEST DIGEST_TEXT COUNT_STAR -test 78b80220002834f612d11b0663f64d59 EXPLAIN SELECT * FROM `test` . `v1` 1 -test 9649c572f7c7b927e314d31620294e34 EXPLAIN SELECT * FROM `test` . `v1` WHERE `a` = ? 1 -test 304c0393779f7b183065e7b577f1be26 EXPLAIN SELECT * FROM `test` . `v1` WHERE `b` > ? 1 -test 6e400ce1796d40cfefa45333d6e5895c EXPLAIN SELECT `a` , `b` FROM `test` . `v1` 1 -test 36c8726233a5c621742d35107d72e5e0 EXPLAIN SELECT `b` , `a` FROM `test` . `v1` 1 -test 0aeb23572eed79a9e05cafe0e9cd1909 SELECT * FROM `test` . `v1` 1 -test 28bd92caf5c189316fab14a67b622203 SELECT * FROM `test` . `v1` WHERE `a` = ? 1 -test 637dba52704594bc4275ba3f37b8f851 SELECT * FROM `test` . `v1` WHERE `b` > ? 1 -test fd8e83e523b0eec97a94eef612154591 SELECT `a` , `b` FROM `test` . `v1` 1 -test c58ed156113959965ebf619b6dd3a8b2 SELECT `b` , `a` FROM `test` . `v1` 1 -test 4d9d22440ce86533e3fac764ab259bbd TRUNCATE TABLE `performance_schema` . `events_statements_summary_by_digest` 1 +test b662d7ab4e8aa67e3aaeb81957a38e97 EXPLAIN SELECT * FROM `test` . `v1` 1 +test 4a6602212e0e1386dada25a12dc2315f EXPLAIN SELECT * FROM `test` . `v1` WHERE `a` = ? 1 +test 7b3a13b2c268ba0a72aade33fb3b9d7d EXPLAIN SELECT * FROM `test` . `v1` WHERE `b` > ? 1 +test 840a987076e3bd7b25603240595f47b9 EXPLAIN SELECT `a` , `b` FROM `test` . `v1` 1 +test 955679ae1c2068ade1003c820f1cdd58 EXPLAIN SELECT `b` , `a` FROM `test` . `v1` 1 +test 3fe51e497b828a521e0b404f7c0e0ac6 SELECT * FROM `test` . `v1` 1 +test 2bec69e086533e335e311b159eee104a SELECT * FROM `test` . `v1` WHERE `a` = ? 1 +test 4e995969bf7999998047ed967d8ebf45 SELECT * FROM `test` . `v1` WHERE `b` > ? 1 +test 51f783c563f3af0cdca3065baace1236 SELECT `a` , `b` FROM `test` . `v1` 1 +test be34ebc622c2d81afca10cc511a06af9 SELECT `b` , `a` FROM `test` . `v1` 1 +test af6cd26b7f88eafb1ff05602a49ce39e TRUNCATE TABLE `performance_schema` . `events_statements_summary_by_digest` 1 DROP TABLE test.v1; CREATE VIEW test.v1 AS SELECT * FROM test.t1; EXPLAIN SELECT * from test.v1; @@ -248,19 +248,19 @@ SELECT SCHEMA_NAME, DIGEST, DIGEST_TEXT, COUNT_STAR FROM performance_schema.events_statements_summary_by_digest ORDER BY DIGEST_TEXT; SCHEMA_NAME DIGEST DIGEST_TEXT COUNT_STAR -test 4ccb56972e9c19941d4928d31502e796 CREATE VIEW `test` . `v1` AS SELECT * FROM `test` . `t1` 1 -test a087be31e6440102676ef0171b8b2734 DROP TABLE `test` . `v1` 1 -test 78b80220002834f612d11b0663f64d59 EXPLAIN SELECT * FROM `test` . `v1` 2 -test 9649c572f7c7b927e314d31620294e34 EXPLAIN SELECT * FROM `test` . `v1` WHERE `a` = ? 2 -test 304c0393779f7b183065e7b577f1be26 EXPLAIN SELECT * FROM `test` . `v1` WHERE `b` > ? 2 -test 6e400ce1796d40cfefa45333d6e5895c EXPLAIN SELECT `a` , `b` FROM `test` . `v1` 2 -test 36c8726233a5c621742d35107d72e5e0 EXPLAIN SELECT `b` , `a` FROM `test` . `v1` 2 -test 0aeb23572eed79a9e05cafe0e9cd1909 SELECT * FROM `test` . `v1` 2 -test 28bd92caf5c189316fab14a67b622203 SELECT * FROM `test` . `v1` WHERE `a` = ? 2 -test 637dba52704594bc4275ba3f37b8f851 SELECT * FROM `test` . `v1` WHERE `b` > ? 2 -test 765bf27a2d45249dcc6377bcc02e1c4b SELECT SCHEMA_NAME , `DIGEST` , `DIGEST_TEXT` , `COUNT_STAR` FROM `performance_schema` . `events_statements_summary_by_digest` ORDER BY `DIGEST_TEXT` 1 -test fd8e83e523b0eec97a94eef612154591 SELECT `a` , `b` FROM `test` . `v1` 2 -test c58ed156113959965ebf619b6dd3a8b2 SELECT `b` , `a` FROM `test` . `v1` 2 -test 4d9d22440ce86533e3fac764ab259bbd TRUNCATE TABLE `performance_schema` . `events_statements_summary_by_digest` 1 +test 1c3d8fb894f91a3cca115966b8c99921 CREATE VIEW `test` . `v1` AS SELECT * FROM `test` . `t1` 1 +test 6b0df2c35a3d2ac98a4e4b43a6e78fe2 DROP TABLE `test` . `v1` 1 +test b662d7ab4e8aa67e3aaeb81957a38e97 EXPLAIN SELECT * FROM `test` . `v1` 2 +test 4a6602212e0e1386dada25a12dc2315f EXPLAIN SELECT * FROM `test` . `v1` WHERE `a` = ? 2 +test 7b3a13b2c268ba0a72aade33fb3b9d7d EXPLAIN SELECT * FROM `test` . `v1` WHERE `b` > ? 2 +test 840a987076e3bd7b25603240595f47b9 EXPLAIN SELECT `a` , `b` FROM `test` . `v1` 2 +test 955679ae1c2068ade1003c820f1cdd58 EXPLAIN SELECT `b` , `a` FROM `test` . `v1` 2 +test 3fe51e497b828a521e0b404f7c0e0ac6 SELECT * FROM `test` . `v1` 2 +test 2bec69e086533e335e311b159eee104a SELECT * FROM `test` . `v1` WHERE `a` = ? 2 +test 4e995969bf7999998047ed967d8ebf45 SELECT * FROM `test` . `v1` WHERE `b` > ? 2 +test 68543d0eacb67ded0fe3280a7be99f73 SELECT SCHEMA_NAME , `DIGEST` , `DIGEST_TEXT` , `COUNT_STAR` FROM `performance_schema` . `events_statements_summary_by_digest` ORDER BY `DIGEST_TEXT` 1 +test 51f783c563f3af0cdca3065baace1236 SELECT `a` , `b` FROM `test` . `v1` 2 +test be34ebc622c2d81afca10cc511a06af9 SELECT `b` , `a` FROM `test` . `v1` 2 +test af6cd26b7f88eafb1ff05602a49ce39e TRUNCATE TABLE `performance_schema` . `events_statements_summary_by_digest` 1 DROP VIEW test.v1; DROP TABLE test.t1; diff --git a/mysql-test/suite/perfschema/r/start_server_low_digest_sql_length.result b/mysql-test/suite/perfschema/r/start_server_low_digest_sql_length.result index 8772d7bf003..9d6925daabf 100644 --- a/mysql-test/suite/perfschema/r/start_server_low_digest_sql_length.result +++ b/mysql-test/suite/perfschema/r/start_server_low_digest_sql_length.result @@ -8,5 +8,5 @@ SELECT 1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1 #################################### SELECT event_name, digest, digest_text, sql_text FROM events_statements_history_long; event_name digest digest_text sql_text -statement/sql/select be5dd6a08f2e34c86168c154f61cbf8c SELECT ? + ? + SELECT ... -statement/sql/truncate e2b84d4b47baf073fa24b65f724e9431 TRUNCATE TABLE truncat... +statement/sql/select 141772a2d94a8dd7d4795ffcf2cb1e6c SELECT ? + ? + SELECT ... +statement/sql/truncate 188ff5124e67b3e6d57b8d5b7fd10694 TRUNCATE TABLE truncat... diff --git a/mysql-test/suite/rpl/include/rpl_gtid_until_before_after_gtids.test b/mysql-test/suite/rpl/include/rpl_gtid_until_before_after_gtids.test new file mode 100644 index 00000000000..4f907d0b012 --- /dev/null +++ b/mysql-test/suite/rpl/include/rpl_gtid_until_before_after_gtids.test @@ -0,0 +1,375 @@ +# +# Helper file to run through test cases to validate that the replica will stop +# at the correct place when running STOP SLAVE UNTIL with options +# SQL_BEFORE_GTIDS and SQL_AFTER_GTIDS. +# +# MTR Parameters: +# ssu_before_gtids (Boolean): Indicates whether to test SQL_BEFORE_GTIDS, +# (when true), or SQL_AFTER_GTIDS (when false). +# + +--let $include_filename= rpl_gtid_until_before_after_gtids.inc +--source include/begin_include_file.inc + +if ($ssu_before_gtids) +{ + --let $ssu_opt=SQL_BEFORE_GTIDS +} +if (!$ssu_before_gtids) +{ + --let $ssu_opt=SQL_AFTER_GTIDS +} + +--echo # +--echo # Test Setup ($ssu_opt) + +--echo # Clean primary and replica states +--connection master +--source include/save_master_gtid.inc +--connection slave +--source include/stop_slave.inc +--source include/start_slave.inc +--source include/sync_with_master_gtid.inc +--source include/stop_slave.inc +--source include/reset_slave.inc +--connection master +RESET MASTER; +set session gtid_domain_id=0; + +--echo # Initialize test data +--connection master +create table t1 (a int); +create table t2 (a int); + +# Set the value counters to use on insertions. Note they are only set once per +# test, subsequent invocations of this .inc file continue to increment the +# previous values. +if (!$t1_ctr) +{ + --let $t1_ctr= 100 + --let $t2_ctr= 200 +} + +--source include/save_master_gtid.inc + +--connection slave +--source include/start_slave.inc +--source include/sync_with_master_gtid.inc + + +--echo # +--echo # Test Case 1 ($ssu_opt): For a single-dimensional binlog state and a +--echo # STOP SLAVE UNTIL gtid position with one GTID, the replica should +if ($ssu_before_gtids) +{ +--echo # execute events up until the GTID is encountered, and immediately stop +} +if (!$ssu_before_gtids) +{ +--echo # execute events up until the GTID is encountered, finish replicating +--echo # that event group, and then stop +} + +--connection slave +--source include/stop_slave.inc +--let $initial_slave_pos= query_get_value(SHOW ALL SLAVES STATUS, Exec_Master_Log_Pos, 1) + +--connection master +set session gtid_domain_id=0; +--eval INSERT INTO t1 VALUES ($t1_ctr) +--inc $t1_ctr +if ($ssu_before_gtids) +{ + --let $expected_stop_gtid= `SELECT @@gtid_binlog_pos` +} +--eval INSERT INTO t1 VALUES ($t1_ctr) +--inc $t1_ctr +--let $until_gtid= `SELECT @@gtid_binlog_pos` +if (!$ssu_before_gtids) +{ + --let $expected_stop_gtid= `SELECT @@gtid_binlog_pos` +} +--eval INSERT INTO t1 VALUES ($t1_ctr) +--inc $t1_ctr + +--connection slave +--eval START SLAVE UNTIL $ssu_opt="$until_gtid" + +--echo # Ensure the slave started +--let $slave_param= Exec_Master_Log_Pos +--let $slave_param_comparison= != +--let $slave_param_value= $initial_slave_pos +--source include/wait_for_slave_param.inc +--let $slave_param_comparison= = + +--source include/wait_for_slave_to_stop.inc +--let $actual_stop_gtid= `SELECT @@gtid_slave_pos` + +if (`SELECT strcmp("$expected_stop_gtid","$actual_stop_gtid") != 0`) +{ + --echo # Expected stop gtid != actual stop gtid ($expected_stop_gtid != $actual_stop_gtid) + --die Expected stop gtid != actual stop gtid +} + +--echo # Clean replica state +--connection master +--source include/save_master_gtid.inc +--connection slave +--source include/start_slave.inc +--source include/sync_with_master_gtid.inc + + +--echo # +--echo # Test Case 2 ($ssu_opt): If a provided until GTID doesn't exist in the +--echo # binary log due to a gap, once an event is seen that is beyond the +--echo # until GTID, the slave should immediately stop. Note the behavior of +--echo # this test case should be the same between SQL_BEFORE_GTIDS and +--echo # SQL_AFTER_GTIDS. + +--connection slave +--let $initial_slave_pos= query_get_value(SHOW ALL SLAVES STATUS, Exec_Master_Log_Pos, 1) +--source include/stop_slave.inc + +--connection master +set session gtid_domain_id=0; +--eval INSERT INTO t1 VALUES ($t1_ctr) +--inc $t1_ctr +--eval INSERT INTO t1 VALUES ($t1_ctr) +--inc $t1_ctr + + +--echo # Skip a seq_no +--let $binlog_pos= `SELECT @@gtid_binlog_pos` +--let $domain_id= `SELECT @@gtid_domain_id` +--let $server_id= `SELECT @@server_id` +--let $last_seq_no= `SELECT REGEXP_SUBSTR('$binlog_pos','[0-9]+\\\$')` + +--let $skipped_seq_no= `SELECT ($last_seq_no + 1)` +--let $new_seq_no= `SELECT ($skipped_seq_no + 1)` +--eval set @@session.gtid_seq_no= $new_seq_no + +--let $until_gtid= $domain_id-$server_id-$skipped_seq_no +--let $expected_stop_gtid= $binlog_pos + +--eval INSERT INTO t1 VALUES ($t1_ctr) +--inc $t1_ctr + +--connection slave +--eval START SLAVE UNTIL $ssu_opt="$until_gtid" + +--echo # Ensure the slave started +--let $slave_param= Exec_Master_Log_Pos +--let $slave_param_comparison= != +--let $slave_param_value= $initial_slave_pos +--source include/wait_for_slave_param.inc +--let $slave_param_comparison= = + +--source include/wait_for_slave_to_stop.inc +--let $actual_stop_gtid= `SELECT @@gtid_slave_pos` + +if (`SELECT strcmp("$expected_stop_gtid","$actual_stop_gtid") != 0`) +{ + --echo # Expected stop gtid != actual stop gtid ($expected_stop_gtid != $actual_stop_gtid) + --die Expected stop gtid != actual stop gtid +} + +--connection slave +--source include/start_slave.inc +--connection master +--sync_slave_with_master + + +--echo # +--echo # Test Case 3 ($ssu_opt): For a multi-dimensional binlog state and a +--echo # STOP SLAVE UNTIL gtid position with one GTID, the replica should +--echo # execute events from only the specified domain until the provided GTID +if ($ssu_before_gtids) +{ +--echo # is encountered, and immediately stop +} +if (!$ssu_before_gtids) +{ +--echo # is encountered, finish replicating that event group, and then stop +} + +--connection slave +--source include/stop_slave.inc +--let $initial_slave_pos= query_get_value(SHOW ALL SLAVES STATUS, Exec_Master_Log_Pos, 1) + +--connection master +set session gtid_domain_id=0; +--eval INSERT INTO t1 VALUES ($t1_ctr) +--inc $t1_ctr + +set session gtid_domain_id=1; +--eval INSERT INTO t2 VALUES ($t2_ctr) +--inc $t2_ctr +--eval INSERT INTO t2 VALUES ($t2_ctr) +--inc $t2_ctr + +if ($ssu_before_gtids) +{ + # Will have GTIDs for both domains 0 and 1 + --let $binlog_pos= `SELECT @@gtid_binlog_pos` + --let $expected_stop_gtid= `SELECT REGEXP_SUBSTR('$binlog_pos','0-[0-9]+-[0-9]+')` +} + +set session gtid_domain_id=0; +--eval INSERT INTO t1 VALUES ($t1_ctr) +--inc $t1_ctr + +--let $binlog_pos= `SELECT @@gtid_binlog_pos` + +# Just the GTID for domain 0 +--let $until_gtid= `SELECT REGEXP_SUBSTR('$binlog_pos','0-[0-9]+-[0-9]+')` + +if (!$ssu_before_gtids) +{ + --let $expected_stop_gtid= $until_gtid +} + +set session gtid_domain_id=1; +--eval INSERT INTO t2 VALUES ($t2_ctr) +--inc $t2_ctr +set session gtid_domain_id=0; + +--connection slave +--eval START SLAVE UNTIL $ssu_opt="$until_gtid" + +--echo # Ensure the slave started +--let $slave_param= Exec_Master_Log_Pos +--let $slave_param_comparison= != +--let $slave_param_value= $initial_slave_pos +--source include/wait_for_slave_param.inc +--let $slave_param_comparison= = + +--source include/wait_for_slave_to_stop.inc +--let $actual_stop_gtid= `SELECT @@gtid_slave_pos` + +if (`SELECT strcmp("$expected_stop_gtid","$actual_stop_gtid") != 0`) +{ + --echo # Expected stop gtid != actual stop gtid ($expected_stop_gtid != $actual_stop_gtid) + --die Expected stop gtid != actual stop gtid +} + +--connection slave +--source include/start_slave.inc +--connection master +--sync_slave_with_master + + +--echo # +--echo # Test Case 4 ($ssu_opt): For a multi-dimensional binlog state and a +--echo # STOP SLAVE UNTIL gtid position with multiple GTIDs, the replica should +if ($ssu_before_gtids) +{ +--echo # for each domain, execute events only up until its provided GTID, and +--echo # once all domains have hit their end point, immediately stop. +} +if (!$ssu_before_gtids) +{ +--echo # stop executing events as soon as all listed GTIDs in the UNTIL list +--echo # have been executed. +} + +--connection slave +--source include/stop_slave.inc +--let $initial_slave_pos= query_get_value(SHOW ALL SLAVES STATUS, Exec_Master_Log_Pos, 1) + +--connection master +--eval SET STATEMENT gtid_domain_id=0 FOR INSERT INTO t1 VALUES ($t1_ctr) +--inc $t1_ctr + +if ($ssu_before_gtids) +{ + # Save binlog pos for domain 0 + --let $expected_stop_gtid_d0= `SELECT REGEXP_SUBSTR(@@global.gtid_binlog_pos,'0-[0-9]+-[0-9]+')` + --echo # Tagging domain 0 stop: $expected_stop_gtid_d0 +} + +--eval SET STATEMENT gtid_domain_id=0 FOR INSERT INTO t1 VALUES ($t1_ctr) +--inc $t1_ctr + +--eval SET STATEMENT gtid_domain_id=1 FOR INSERT INTO t2 VALUES ($t2_ctr) +--inc $t2_ctr + +if ($ssu_before_gtids) +{ + # Save binlog pos for domain 1 + --let $expected_stop_gtid_d1= `SELECT REGEXP_SUBSTR(@@global.gtid_binlog_pos,'1-[0-9]+-[0-9]+')` + --let $expected_stop_gtid= $expected_stop_gtid_d0,$expected_stop_gtid_d1 +} + +--eval SET STATEMENT gtid_domain_id=1 FOR INSERT INTO t2 VALUES ($t2_ctr) +--inc $t2_ctr + +--let $until_gtid= `SELECT @@gtid_binlog_pos` +if (!$ssu_before_gtids) +{ + --let $expected_stop_gtid= $until_gtid +} + +--eval SET STATEMENT gtid_domain_id=0 FOR INSERT INTO t1 VALUES ($t1_ctr) +--inc $t1_ctr +--eval SET STATEMENT gtid_domain_id=1 FOR INSERT INTO t2 VALUES ($t2_ctr) +--inc $t2_ctr + +--connection slave +--eval START SLAVE UNTIL $ssu_opt="$until_gtid" + +--echo # Ensure the slave started +--let $slave_param= Exec_Master_Log_Pos +--let $slave_param_comparison= != +--let $slave_param_value= $initial_slave_pos +--source include/wait_for_slave_param.inc +--let $slave_param_comparison= = + +--source include/wait_for_slave_to_stop.inc +--let $actual_stop_gtid= `SELECT @@gtid_slave_pos` + +if (`SELECT strcmp("$expected_stop_gtid","$actual_stop_gtid") != 0`) +{ + --echo # Expected stop gtid != actual stop gtid ($expected_stop_gtid != $actual_stop_gtid) + --die Expected stop gtid != actual stop gtid +} + +--connection slave +--source include/start_slave.inc +--connection master +--sync_slave_with_master + + +--echo # +--echo # Error Case 1: Not providing a valid GTID should result in a syntax +--echo # error + +--connection slave +--source include/stop_slave.inc + +--error ER_INCORRECT_GTID_STATE +--eval START SLAVE UNTIL $ssu_opt="a" + +--error ER_INCORRECT_GTID_STATE +--eval START SLAVE UNTIL $ssu_opt="0" + +--error ER_INCORRECT_GTID_STATE +--eval START SLAVE UNTIL $ssu_opt="0-1" + +--error ER_INCORRECT_GTID_STATE +--eval START SLAVE UNTIL $ssu_opt="0-1-" + +--error ER_INCORRECT_GTID_STATE +--eval START SLAVE UNTIL $ssu_opt="a-b-c" + +--source include/start_slave.inc + +--echo # +--echo # Cleanup test data +--connection master +DROP TABLE t1, t2; +--source include/save_master_gtid.inc +--connection slave +--source include/sync_with_master_gtid.inc + +--let $include_filename= rpl_gtid_until_before_after_gtids.inc +--source include/end_include_file.inc diff --git a/mysql-test/suite/rpl/r/rpl_gtid_until_before_after_gtids.result b/mysql-test/suite/rpl/r/rpl_gtid_until_before_after_gtids.result new file mode 100644 index 00000000000..dd9035d3680 --- /dev/null +++ b/mysql-test/suite/rpl/r/rpl_gtid_until_before_after_gtids.result @@ -0,0 +1,239 @@ +include/master-slave.inc +[connection master] +include/rpl_gtid_until_before_after_gtids.inc +# +# Test Setup (SQL_BEFORE_GTIDS) +# Clean primary and replica states +connection master; +connection slave; +connection master; +RESET MASTER; +set session gtid_domain_id=0; +# Initialize test data +connection master; +create table t1 (a int); +create table t2 (a int); +connection slave; +# +# Test Case 1 (SQL_BEFORE_GTIDS): For a single-dimensional binlog state and a +# STOP SLAVE UNTIL gtid position with one GTID, the replica should +# execute events up until the GTID is encountered, and immediately stop +connection slave; +connection master; +set session gtid_domain_id=0; +INSERT INTO t1 VALUES (100); +INSERT INTO t1 VALUES (101); +INSERT INTO t1 VALUES (102); +connection slave; +START SLAVE UNTIL SQL_BEFORE_GTIDS="0-1-4"; +# Ensure the slave started +# Clean replica state +connection master; +connection slave; +# +# Test Case 2 (SQL_BEFORE_GTIDS): If a provided until GTID doesn't exist in the +# binary log due to a gap, once an event is seen that is beyond the +# until GTID, the slave should immediately stop. Note the behavior of +# this test case should be the same between SQL_BEFORE_GTIDS and +# SQL_AFTER_GTIDS. +connection slave; +connection master; +set session gtid_domain_id=0; +INSERT INTO t1 VALUES (103); +INSERT INTO t1 VALUES (104); +# Skip a seq_no +set @@session.gtid_seq_no= 9; +INSERT INTO t1 VALUES (105); +connection slave; +START SLAVE UNTIL SQL_BEFORE_GTIDS="0-1-8"; +# Ensure the slave started +connection slave; +connection master; +connection slave; +# +# Test Case 3 (SQL_BEFORE_GTIDS): For a multi-dimensional binlog state and a +# STOP SLAVE UNTIL gtid position with one GTID, the replica should +# execute events from only the specified domain until the provided GTID +# is encountered, and immediately stop +connection slave; +connection master; +set session gtid_domain_id=0; +INSERT INTO t1 VALUES (106); +set session gtid_domain_id=1; +INSERT INTO t2 VALUES (200); +INSERT INTO t2 VALUES (201); +set session gtid_domain_id=0; +INSERT INTO t1 VALUES (107); +set session gtid_domain_id=1; +INSERT INTO t2 VALUES (202); +set session gtid_domain_id=0; +connection slave; +START SLAVE UNTIL SQL_BEFORE_GTIDS="0-1-11"; +# Ensure the slave started +connection slave; +connection master; +connection slave; +# +# Test Case 4 (SQL_BEFORE_GTIDS): For a multi-dimensional binlog state and a +# STOP SLAVE UNTIL gtid position with multiple GTIDs, the replica should +# for each domain, execute events only up until its provided GTID, and +# once all domains have hit their end point, immediately stop. +connection slave; +connection master; +SET STATEMENT gtid_domain_id=0 FOR INSERT INTO t1 VALUES (108); +# Tagging domain 0 stop: 0-1-12 +SET STATEMENT gtid_domain_id=0 FOR INSERT INTO t1 VALUES (109); +SET STATEMENT gtid_domain_id=1 FOR INSERT INTO t2 VALUES (203); +SET STATEMENT gtid_domain_id=1 FOR INSERT INTO t2 VALUES (204); +SET STATEMENT gtid_domain_id=0 FOR INSERT INTO t1 VALUES (110); +SET STATEMENT gtid_domain_id=1 FOR INSERT INTO t2 VALUES (205); +connection slave; +START SLAVE UNTIL SQL_BEFORE_GTIDS="0-1-13,1-1-5"; +# Ensure the slave started +connection slave; +connection master; +connection slave; +# +# Error Case 1: Not providing a valid GTID should result in a syntax +# error +connection slave; +START SLAVE UNTIL SQL_BEFORE_GTIDS="a"; +ERROR HY000: Could not parse GTID list +START SLAVE UNTIL SQL_BEFORE_GTIDS="0"; +ERROR HY000: Could not parse GTID list +START SLAVE UNTIL SQL_BEFORE_GTIDS="0-1"; +ERROR HY000: Could not parse GTID list +START SLAVE UNTIL SQL_BEFORE_GTIDS="0-1-"; +ERROR HY000: Could not parse GTID list +START SLAVE UNTIL SQL_BEFORE_GTIDS="a-b-c"; +ERROR HY000: Could not parse GTID list +# +# Cleanup test data +connection master; +DROP TABLE t1, t2; +connection slave; +include/rpl_gtid_until_before_after_gtids.inc +# +# Test Setup (SQL_AFTER_GTIDS) +# Clean primary and replica states +connection master; +connection slave; +connection master; +RESET MASTER; +set session gtid_domain_id=0; +# Initialize test data +connection master; +create table t1 (a int); +create table t2 (a int); +connection slave; +# +# Test Case 1 (SQL_AFTER_GTIDS): For a single-dimensional binlog state and a +# STOP SLAVE UNTIL gtid position with one GTID, the replica should +# execute events up until the GTID is encountered, finish replicating +# that event group, and then stop +connection slave; +connection master; +set session gtid_domain_id=0; +INSERT INTO t1 VALUES (111); +INSERT INTO t1 VALUES (112); +INSERT INTO t1 VALUES (113); +connection slave; +START SLAVE UNTIL SQL_AFTER_GTIDS="0-1-4"; +# Ensure the slave started +# Clean replica state +connection master; +connection slave; +# +# Test Case 2 (SQL_AFTER_GTIDS): If a provided until GTID doesn't exist in the +# binary log due to a gap, once an event is seen that is beyond the +# until GTID, the slave should immediately stop. Note the behavior of +# this test case should be the same between SQL_BEFORE_GTIDS and +# SQL_AFTER_GTIDS. +connection slave; +connection master; +set session gtid_domain_id=0; +INSERT INTO t1 VALUES (114); +INSERT INTO t1 VALUES (115); +# Skip a seq_no +set @@session.gtid_seq_no= 9; +INSERT INTO t1 VALUES (116); +connection slave; +START SLAVE UNTIL SQL_AFTER_GTIDS="0-1-8"; +# Ensure the slave started +connection slave; +connection master; +connection slave; +# +# Test Case 3 (SQL_AFTER_GTIDS): For a multi-dimensional binlog state and a +# STOP SLAVE UNTIL gtid position with one GTID, the replica should +# execute events from only the specified domain until the provided GTID +# is encountered, finish replicating that event group, and then stop +connection slave; +connection master; +set session gtid_domain_id=0; +INSERT INTO t1 VALUES (117); +set session gtid_domain_id=1; +INSERT INTO t2 VALUES (206); +INSERT INTO t2 VALUES (207); +set session gtid_domain_id=0; +INSERT INTO t1 VALUES (118); +set session gtid_domain_id=1; +INSERT INTO t2 VALUES (208); +set session gtid_domain_id=0; +connection slave; +START SLAVE UNTIL SQL_AFTER_GTIDS="0-1-11"; +# Ensure the slave started +connection slave; +connection master; +connection slave; +# +# Test Case 4 (SQL_AFTER_GTIDS): For a multi-dimensional binlog state and a +# STOP SLAVE UNTIL gtid position with multiple GTIDs, the replica should +# stop executing events as soon as all listed GTIDs in the UNTIL list +# have been executed. +connection slave; +connection master; +SET STATEMENT gtid_domain_id=0 FOR INSERT INTO t1 VALUES (119); +SET STATEMENT gtid_domain_id=0 FOR INSERT INTO t1 VALUES (120); +SET STATEMENT gtid_domain_id=1 FOR INSERT INTO t2 VALUES (209); +SET STATEMENT gtid_domain_id=1 FOR INSERT INTO t2 VALUES (210); +SET STATEMENT gtid_domain_id=0 FOR INSERT INTO t1 VALUES (121); +SET STATEMENT gtid_domain_id=1 FOR INSERT INTO t2 VALUES (211); +connection slave; +START SLAVE UNTIL SQL_AFTER_GTIDS="0-1-13,1-1-5"; +# Ensure the slave started +connection slave; +connection master; +connection slave; +# +# Error Case 1: Not providing a valid GTID should result in a syntax +# error +connection slave; +START SLAVE UNTIL SQL_AFTER_GTIDS="a"; +ERROR HY000: Could not parse GTID list +START SLAVE UNTIL SQL_AFTER_GTIDS="0"; +ERROR HY000: Could not parse GTID list +START SLAVE UNTIL SQL_AFTER_GTIDS="0-1"; +ERROR HY000: Could not parse GTID list +START SLAVE UNTIL SQL_AFTER_GTIDS="0-1-"; +ERROR HY000: Could not parse GTID list +START SLAVE UNTIL SQL_AFTER_GTIDS="a-b-c"; +ERROR HY000: Could not parse GTID list +# +# Cleanup test data +connection master; +DROP TABLE t1, t2; +connection slave; +# +# Error Case 2: Providing both SQL_BEFORE_GTIDS and SQL_AFTER_GTIDS +# should result in a syntax error +connection slave; +START SLAVE UNTIL SQL_AFTER_GTIDS="0-1-1" SQL_BEFORE_GTIDS="0-1-1"; +ERROR 42000: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near 'SQL_BEFORE_GTIDS="0-1-1"' at line 1 +# +# Cleanup +include/save_master_gtid.inc +connection slave; +include/sync_with_master_gtid.inc +include/rpl_end.inc +# End of rpl_gtid_until_before_gtids.test diff --git a/mysql-test/suite/rpl/t/rpl_gtid_until_before_after_gtids.test b/mysql-test/suite/rpl/t/rpl_gtid_until_before_after_gtids.test new file mode 100644 index 00000000000..cf245ca01a3 --- /dev/null +++ b/mysql-test/suite/rpl/t/rpl_gtid_until_before_after_gtids.test @@ -0,0 +1,45 @@ +# +# This test validates the behavior of SQL_BEFORE_GTIDS and SQL_AFTER_GTIDS +# of a slave's START SLAVE UNTIL command. Notably, it tests the following +# scenarios: +# 1. Single domain id in binary log with a single domain id in the UNTIL +# condition +# 2. Multiple domain ids in binary log with a single domain id in the UNTIL +# condition +# 3. Multiple domain ids in binary log with multiple domain ids in the UNTIL +# condition +# 4. A gap in the binary log with the UNTIL condition GTID pointed to the +# missing transaction +# 5. Syntax errors using the new options +# +# +# References: +# MDEV-27247: Add keywords "exclusive" and "inclusive" for START SLAVE UNTIL +# + +--source include/have_innodb.inc +--source include/have_log_bin.inc +--source include/master-slave.inc + +--let $ssu_before_gtids=1 +--source include/rpl_gtid_until_before_after_gtids.test + +--let $ssu_before_gtids=0 +--source include/rpl_gtid_until_before_after_gtids.test + +--echo # +--echo # Error Case 2: Providing both SQL_BEFORE_GTIDS and SQL_AFTER_GTIDS +--echo # should result in a syntax error +--connection slave +--error ER_PARSE_ERROR +START SLAVE UNTIL SQL_AFTER_GTIDS="0-1-1" SQL_BEFORE_GTIDS="0-1-1"; + +--echo # +--echo # Cleanup +--source include/save_master_gtid.inc + +--connection slave +--source include/sync_with_master_gtid.inc + +--source include/rpl_end.inc +--echo # End of rpl_gtid_until_before_gtids.test diff --git a/sql/lex.h b/sql/lex.h index 18519d1f8d1..fc48ace64dc 100644 --- a/sql/lex.h +++ b/sql/lex.h @@ -618,6 +618,8 @@ SYMBOL symbols[] = { { "SQLEXCEPTION", SYM(SQLEXCEPTION_SYM)}, { "SQLSTATE", SYM(SQLSTATE_SYM)}, { "SQLWARNING", SYM(SQLWARNING_SYM)}, + { "SQL_AFTER_GTIDS", SYM(SQL_AFTER_GTIDS_SYM)}, + { "SQL_BEFORE_GTIDS", SYM(SQL_BEFORE_GTIDS_SYM)}, { "SQL_BIG_RESULT", SYM(SQL_BIG_RESULT)}, { "SQL_BUFFER_RESULT", SYM(SQL_BUFFER_RESULT)}, { "SQL_CACHE", SYM(SQL_CACHE_SYM)}, diff --git a/sql/rpl_rli.cc b/sql/rpl_rli.cc index e0a272324e6..bc1f0ebbff5 100644 --- a/sql/rpl_rli.cc +++ b/sql/rpl_rli.cc @@ -60,7 +60,8 @@ Relay_log_info::Relay_log_info(bool is_slave_recovery, const char* thread_name) abort_pos_wait(0), slave_run_id(0), sql_driver_thd(), gtid_skip_flag(GTID_SKIP_NOT), inited(0), abort_slave(0), stop_for_until(0), slave_running(MYSQL_SLAVE_NOT_RUN), until_condition(UNTIL_NONE), - until_log_pos(0), retried_trans(0), executed_entries(0), + until_log_pos(0), is_until_before_gtids(false), + retried_trans(0), executed_entries(0), last_trans_retry_count(0), sql_delay(0), sql_delay_end(0), until_relay_log_names_defer(false), m_flags(0) diff --git a/sql/rpl_rli.h b/sql/rpl_rli.h index 6d6c098847b..6ccddbc623a 100644 --- a/sql/rpl_rli.h +++ b/sql/rpl_rli.h @@ -338,6 +338,8 @@ public: /* Condition for UNTIL master_gtid_pos. */ slave_connection_state until_gtid_pos; + bool is_until_before_gtids; + /* retried_trans is a cumulative counter: how many times the slave has retried a transaction (any) since slave started. diff --git a/sql/slave.cc b/sql/slave.cc index 588b73b5511..8ed28dec619 100644 --- a/sql/slave.cc +++ b/sql/slave.cc @@ -2525,6 +2525,42 @@ after_set_capability: goto err; } } + + query_str.length(0); + if (query_str.append( + STRING_WITH_LEN("SET @slave_gtid_until_before_gtids="), + system_charset_info) || + query_str.append_ulonglong(mi->rli.is_until_before_gtids)) + { + err_code= ER_OUTOFMEMORY; + errmsg= + "The slave I/O thread stops because a fatal out-of-memory error " + "is encountered when it tries to set " + "@slave_gtid_until_before_gtids."; + sprintf(err_buff, "%s Error: Out of memory", errmsg); + goto err; + } + + rc= mysql_real_query(mysql, query_str.ptr(), query_str.length()); + if (unlikely(rc)) + { + err_code= mysql_errno(mysql); + if (is_network_error(err_code)) + { + mi->report(ERROR_LEVEL, err_code, NULL, + "Setting @slave_gtid_until_before_gtids failed with " + "error: %s", mysql_error(mysql)); + goto network_err; + } + else + { + /* Fatal error */ + errmsg= "The slave I/O thread stops because a fatal error is " + "encountered when it tries to set @slave_gtid_until_before_gtids."; + sprintf(err_buff, "%s Error: %s", errmsg, mysql_error(mysql)); + goto err; + } + } } } else diff --git a/sql/sql_lex.h b/sql/sql_lex.h index 0e4e01becf8..5f3e12b0745 100644 --- a/sql/sql_lex.h +++ b/sql/sql_lex.h @@ -494,6 +494,7 @@ struct LEX_MASTER_INFO float heartbeat_period; int sql_delay; bool is_demotion_opt; + bool is_until_before_gtids; /* Enum is used for making it possible to detect if the user changed variable or if it should be left at old value @@ -537,6 +538,7 @@ struct LEX_MASTER_INFO use_gtid_opt= LEX_GTID_UNCHANGED; sql_delay= -1; is_demotion_opt= 0; + is_until_before_gtids= false; } }; diff --git a/sql/sql_repl.cc b/sql/sql_repl.cc index 1f03338ccce..f3017c3d311 100644 --- a/sql/sql_repl.cc +++ b/sql/sql_repl.cc @@ -162,6 +162,7 @@ struct binlog_send_info { bool clear_initial_log_pos; bool should_stop; size_t dirlen; + bool is_until_before_gtids; binlog_send_info(THD *thd_arg, String *packet_arg, ushort flags_arg, char *lfn) @@ -180,7 +181,7 @@ struct binlog_send_info { hb_info_counter(0), #endif clear_initial_log_pos(false), - should_stop(false) + should_stop(false), is_until_before_gtids(false) { error_text[0] = 0; bzero(&error_gtid, sizeof(error_gtid)); @@ -808,6 +809,18 @@ get_slave_until_gtid(THD *thd, String *out_str) return entry && entry->val_str(&null_value, out_str, 0) && !null_value; } +static bool +get_slave_gtid_until_before_gtids(THD *thd) +{ + bool null_value; + + const LEX_CSTRING name= { STRING_WITH_LEN("slave_gtid_until_before_gtids") }; + user_var_entry *entry= + (user_var_entry*) my_hash_search(&thd->user_vars, (uchar*) name.str, + name.length); + return entry && entry->val_int(&null_value) && !null_value; +} + /* Function prepares and sends repliation heartbeat event. @@ -1867,8 +1880,9 @@ send_event_to_slave(binlog_send_info *info, Log_event_type event_type, This domain already reached the START SLAVE UNTIL stop condition, so skip this event group. */ - info->gtid_skip_group = (flags2 & Gtid_log_event::FL_STANDALONE ? - GTID_SKIP_STANDALONE : GTID_SKIP_TRANSACTION); + info->gtid_skip_group= (flags2 & Gtid_log_event::FL_STANDALONE + ? GTID_SKIP_STANDALONE + : GTID_SKIP_TRANSACTION); } else if (event_gtid.server_id == gtid->server_id && event_gtid.seq_no >= gtid->seq_no) @@ -1885,14 +1899,19 @@ send_event_to_slave(binlog_send_info *info, Log_event_type event_type, info->gtid_until_group= (flags2 & Gtid_log_event::FL_STANDALONE ? GTID_UNTIL_STOP_AFTER_STANDALONE : GTID_UNTIL_STOP_AFTER_TRANSACTION); - if (event_gtid.seq_no > until_seq_no) + if (event_gtid.seq_no > until_seq_no || + info->is_until_before_gtids) { /* + Stop processing events now and skip the current event group + because either: + The GTID in START SLAVE UNTIL condition is missing in our binlog. This should normally not happen (user error), but since we can be sure that we are now beyond the position that the UNTIL condition - should be in, we can just stop now. And we also need to skip this - event group (as it is beyond the UNTIL condition). + should be in, we can just stop now. + + Or the until condition is specified as SQL_BEFORE_GTIDS */ info->gtid_skip_group = (flags2 & Gtid_log_event::FL_STANDALONE ? GTID_SKIP_STANDALONE : GTID_SKIP_TRANSACTION); @@ -2139,7 +2158,10 @@ static int init_binlog_sender(binlog_send_info *info, info->slave_gtid_strict_mode= get_slave_gtid_strict_mode(thd); info->slave_gtid_ignore_duplicates= get_slave_gtid_ignore_duplicates(thd); if (get_slave_until_gtid(thd, &slave_until_gtid_str)) + { info->until_gtid_state= &info->until_gtid_state_obj; + info->is_until_before_gtids= get_slave_gtid_until_before_gtids(thd); + } } DBUG_EXECUTE_IF("binlog_force_reconnect_after_22_events", @@ -3228,6 +3250,7 @@ int start_slave(THD* thd , Master_info* mi, bool net_report) goto err; } mi->rli.until_condition= Relay_log_info::UNTIL_GTID; + mi->rli.is_until_before_gtids= thd->lex->mi.is_until_before_gtids; } else mi->rli.clear_until_condition(); diff --git a/sql/sql_yacc.yy b/sql/sql_yacc.yy index 1adfd37a046..e2904cb1699 100644 --- a/sql/sql_yacc.yy +++ b/sql/sql_yacc.yy @@ -1090,6 +1090,8 @@ bool my_yyoverflow(short **a, YYSTYPE **b, size_t *yystacksize); %token SONAME_SYM %token SOUNDS_SYM %token SOURCE_SYM +%token SQL_AFTER_GTIDS_SYM +%token SQL_BEFORE_GTIDS_SYM %token SQL_BUFFER_RESULT %token SQL_CACHE_SYM %token SQL_CALC_FOUND_ROWS @@ -8062,6 +8064,17 @@ slave_until: | UNTIL_SYM MASTER_GTID_POS_SYM '=' TEXT_STRING_sys { Lex->mi.gtid_pos_str = $4; + Lex->mi.is_until_before_gtids= false; + } + | UNTIL_SYM SQL_AFTER_GTIDS_SYM '=' TEXT_STRING_sys + { + Lex->mi.gtid_pos_str = $4; + Lex->mi.is_until_before_gtids= false; + } + | UNTIL_SYM SQL_BEFORE_GTIDS_SYM '=' TEXT_STRING_sys + { + Lex->mi.gtid_pos_str = $4; + Lex->mi.is_until_before_gtids= true; } ;