From d5c215f26dc7330759eaab400831294bd2830a18 Mon Sep 17 00:00:00 2001 From: Kristian Nielsen Date: Thu, 27 Aug 2026 14:09:43 +0200 Subject: [PATCH] DRAFT: MDEV-36025: Server start fails on mariabackup with prepared transactions mariabackup --no-lock can leave InnoDB prepared transactions in the restored server; this can also happen even without --no-lock when the server is running parallel replication. But mariabackup does not back up the TC_LOG state (typically binlog, or TC_LOG_MMAP if binlog is disabled). This caused the server to complain during startup that the TC_LOG state has been lost, and requires to temporarily configure the server with --tc-heuristic-recover=rollback to roll back the offending transactions. This patch implements a new value --tc-heuristic-recover=auto which makes the server automatically handle rolling back prepared transactions as needed when the TC_LOG state is missing, without the user to change any configurations. This draft patch makes the new value AUTO the default. It is to be determined whether to do this in existing GA releases (eg. 10.11), or whether to make AUTO the default only in some new 13.x release and have the user explicitly configure --tc-heuristic-recover=auto in existing GA releases if they need it. The old behaviour of --tc-heuristic-recover does not seem very useful. If the TC_LOG state is intentionally removed by the user, the correct behaviour seems obviously to be to roll back any prepared transactions. The purpose of having those internal prepared transactions in the first place is to ensure consistency between the engine and the binlog, this has no meaning if the binlog is gone. Or to ensure consistency between engines in multi-engine transactions, and rolling back will ensure this consistency (while commit may not). Note that the prepare step for these transactions is completely internal to the server, and there is no visible semantic different for the user between the backup snapshot occurring just before or just after a transaction ends up as "prepared". Signed-off-by: Kristian Nielsen --- .../slave_provision_prepared_trx.cnf | 14 +++ .../slave_provision_prepared_trx.result | 70 +++++++++++++ .../slave_provision_prepared_trx.test | 99 +++++++++++++++++++ sql/log.cc | 6 +- sql/mysqld.cc | 4 +- sql/sql_class.h | 1 + 6 files changed, 190 insertions(+), 4 deletions(-) create mode 100644 mysql-test/suite/mariabackup/slave_provision_prepared_trx.cnf create mode 100644 mysql-test/suite/mariabackup/slave_provision_prepared_trx.result create mode 100644 mysql-test/suite/mariabackup/slave_provision_prepared_trx.test diff --git a/mysql-test/suite/mariabackup/slave_provision_prepared_trx.cnf b/mysql-test/suite/mariabackup/slave_provision_prepared_trx.cnf new file mode 100644 index 0000000000000..13892d60ef457 --- /dev/null +++ b/mysql-test/suite/mariabackup/slave_provision_prepared_trx.cnf @@ -0,0 +1,14 @@ +[mysqld.1] +log-slave-updates +loose-innodb + +[mysqld.2] +log-slave-updates +loose-innodb +skip-slave-start + +[ENV] +SERVER_MYPORT_1= @mysqld.1.port +SERVER_MYSOCK_1= @mysqld.1.socket +SERVER_MYPORT_2= @mysqld.2.port +SERVER_MYSOCK_2= @mysqld.2.socket diff --git a/mysql-test/suite/mariabackup/slave_provision_prepared_trx.result b/mysql-test/suite/mariabackup/slave_provision_prepared_trx.result new file mode 100644 index 0000000000000..4e20700911363 --- /dev/null +++ b/mysql-test/suite/mariabackup/slave_provision_prepared_trx.result @@ -0,0 +1,70 @@ +### MDEV-36025 backup taken from a slave with optimistic parallel replication fails to restore most of the time +### Test provisioning a slave from an existing server running optimistic +### parallel replication so that prepared transactions are left in the +### backup when the restored server is started. +RESET MASTER; +CREATE TABLE t1(a INT PRIMARY KEY, b VARCHAR(60)) ENGINE INNODB; +INSERT INTO t1 VALUES(1, NULL); +INSERT INTO t1 VALUES(2, NULL); +INSERT INTO t1 VALUES(3, NULL); +connect server2,127.0.0.1,root,,,$SERVER_MYPORT_2; +SET STATEMENT sql_log_bin=0 FOR ALTER TABLE mysql.gtid_slave_pos ENGINE=InnoDB; +RESET MASTER; +SET GLOBAL gtid_slave_pos= ''; +CHANGE MASTER TO +master_port=PORT, master_host='127.0.0.1', master_user='root', +master_use_gtid= slave_pos; +SET GLOBAL slave_parallel_threads=3; +SET GLOBAL slave_parallel_mode=optimistic; +START SLAVE; +connection default; +include/save_master_gtid.inc +connection server2; +include/sync_with_master_gtid.inc +connect server2a,127.0.0.1,root,,,$SERVER_MYPORT_2; +BEGIN; +SELECT * FROM t1 WHERE a=1 FOR UPDATE; +a b +1 NULL +connection default; +UPDATE t1 SET b='xyzzy' WHERE a=1; +INSERT INTO t1 VALUES (4, NULL); +UPDATE t1 SET b='foobar' WHERE a=2; +include/save_master_gtid.inc +*** Doing backup... +*** Doing prepare... +connection server2a; +ROLLBACK; +include/sync_with_master_gtid.inc +connection server2; +*** Provision a new slave from the backup +*** Stopping provisioned server +*** Removing old datadir for provisioned server +*** Provision new server from backup +disconnect server2a; +# restart +CHANGE MASTER TO +master_port=PORT, master_host='127.0.0.1', master_user='root', +master_use_gtid= slave_pos; +START SLAVE; +connection default; +include/save_master_gtid.inc +SELECT * FROM t1 ORDER BY a; +a b +1 xyzzy +2 foobar +3 NULL +4 NULL +connection server2; +include/sync_with_master_gtid.inc +SELECT * FROM t1 ORDER BY a; +a b +1 xyzzy +2 foobar +3 NULL +4 NULL +STOP SLAVE; +RESET SLAVE ALL; +DROP TABLE t1; +connection default; +DROP TABLE t1; diff --git a/mysql-test/suite/mariabackup/slave_provision_prepared_trx.test b/mysql-test/suite/mariabackup/slave_provision_prepared_trx.test new file mode 100644 index 0000000000000..0a396fc2e1c4b --- /dev/null +++ b/mysql-test/suite/mariabackup/slave_provision_prepared_trx.test @@ -0,0 +1,99 @@ +--source include/have_innodb.inc +--source include/have_log_bin.inc + +--echo ### MDEV-36025 backup taken from a slave with optimistic parallel replication fails to restore most of the time +--echo ### Test provisioning a slave from an existing server running optimistic +--echo ### parallel replication so that prepared transactions are left in the +--echo ### backup when the restored server is started. + + +RESET MASTER; +CREATE TABLE t1(a INT PRIMARY KEY, b VARCHAR(60)) ENGINE INNODB; +INSERT INTO t1 VALUES(1, NULL); +INSERT INTO t1 VALUES(2, NULL); +INSERT INTO t1 VALUES(3, NULL); + +--connect (server2,127.0.0.1,root,,,$SERVER_MYPORT_2) +let MYSQLD_DATADIR=`select @@datadir`; +let $basedir=$MYSQLTEST_VARDIR/tmp/backup; +SET STATEMENT sql_log_bin=0 FOR ALTER TABLE mysql.gtid_slave_pos ENGINE=InnoDB; +RESET MASTER; +SET GLOBAL gtid_slave_pos= ''; + +--replace_result $SERVER_MYPORT_1 PORT +eval CHANGE MASTER TO + master_port=$SERVER_MYPORT_1, master_host='127.0.0.1', master_user='root', + master_use_gtid= slave_pos; +SET GLOBAL slave_parallel_threads=3; +SET GLOBAL slave_parallel_mode=optimistic; +START SLAVE; + +--connection default +--source include/save_master_gtid.inc +--connection server2 +--source include/sync_with_master_gtid.inc + +# Block transaction T1 from committing on the slave, forcing a following +# T2 to remain in "prepared" state when the backup is taken. +--connect (server2a,127.0.0.1,root,,,$SERVER_MYPORT_2) +BEGIN; +SELECT * FROM t1 WHERE a=1 FOR UPDATE; + +--connection default +UPDATE t1 SET b='xyzzy' WHERE a=1; +INSERT INTO t1 VALUES (4, NULL); +UPDATE t1 SET b='foobar' WHERE a=2; +--source include/save_master_gtid.inc + +# Sleep a small amount to give a higher chance for the transactions to reach +# their prepared state when the backup runs. +--sleep 0.3 + +--echo *** Doing backup... +--exec $XTRABACKUP --defaults-file=$MYSQLTEST_VARDIR/my.cnf --socket=$SERVER_MYSOCK_2 --backup --target-dir=$basedir +--echo *** Doing prepare... +--exec $XTRABACKUP --prepare --binlog-info=1 --target-dir=$basedir + +--connection server2a +ROLLBACK; +--source include/sync_with_master_gtid.inc + +--connection server2 +--echo *** Provision a new slave from the backup + +--echo *** Stopping provisioned server +--source include/shutdown_mysqld.inc + +--echo *** Removing old datadir for provisioned server +--rmdir $MYSQLD_DATADIR + +--echo *** Provision new server from backup +--exec $XTRABACKUP --copy-back --datadir=$MYSQLD_DATADIR --target-dir=$basedir + +--disconnect server2a +--source include/start_mysqld.inc + +--replace_result $SERVER_MYPORT_1 PORT +eval CHANGE MASTER TO + master_port=$SERVER_MYPORT_1, master_host='127.0.0.1', master_user='root', + master_use_gtid= slave_pos; +# The slave GTID position was saved transactionally to mysql.gtid_slave_pos +# as part of the backup. +START SLAVE; + +--connection default +--source include/save_master_gtid.inc +SELECT * FROM t1 ORDER BY a; + +--connection server2 +--source include/sync_with_master_gtid.inc +SELECT * FROM t1 ORDER BY a; + +STOP SLAVE; +RESET SLAVE ALL; +DROP TABLE t1; + +--connection default +DROP TABLE t1; + +rmdir $basedir; diff --git a/sql/log.cc b/sql/log.cc index 00544f71b17cc..b88cdc72d789b 100644 --- a/sql/log.cc +++ b/sql/log.cc @@ -10039,7 +10039,8 @@ int TC_LOG_MMAP::open(const char *opt_name) inited= 1; crashed= TRUE; sql_print_information("Recovering after a crash using %s", opt_name); - if (tc_heuristic_recover) + if (tc_heuristic_recover && + tc_heuristic_recover != TC_HEURISTIC_RECOVER_AUTO) { sql_print_error("Cannot perform automatic crash recovery when " "--tc-heuristic-recover is used"); @@ -10562,7 +10563,8 @@ TC_LOG_MMAP tc_log_mmap; int TC_LOG::using_heuristic_recover() { - if (!tc_heuristic_recover) + if (!tc_heuristic_recover || + tc_heuristic_recover == TC_HEURISTIC_RECOVER_AUTO) return 0; sql_print_information("Heuristic crash recovery mode"); diff --git a/sql/mysqld.cc b/sql/mysqld.cc index 52938056f0b5d..7d50735fc1195 100644 --- a/sql/mysqld.cc +++ b/sql/mysqld.cc @@ -294,7 +294,7 @@ const char *show_comp_option_name[]= {"YES", "NO", "DISABLED"}; static const char *tc_heuristic_recover_names[]= { - "OFF", "COMMIT", "ROLLBACK", NullS + "OFF", "COMMIT", "ROLLBACK", "AUTO", NullS }; static TYPELIB tc_heuristic_recover_typelib= { @@ -7027,7 +7027,7 @@ struct my_option my_long_options[]= {"tc-heuristic-recover", 0, "Decision to use in heuristic recover process", &tc_heuristic_recover, &tc_heuristic_recover, - &tc_heuristic_recover_typelib, GET_ENUM, REQUIRED_ARG, 0, 0, 0, 0, 0, 0}, + &tc_heuristic_recover_typelib, GET_ENUM, REQUIRED_ARG, 3, 0, 0, 0, 0, 0}, {"temp-pool", 0, #if (ENABLE_TEMP_POOL) "Using this option will cause most temporary files created to use a small " diff --git a/sql/sql_class.h b/sql/sql_class.h index 20b3385b0582d..5d6815a99ce8b 100644 --- a/sql/sql_class.h +++ b/sql/sql_class.h @@ -280,6 +280,7 @@ class Recreate_info #define TC_HEURISTIC_RECOVER_COMMIT 1 #define TC_HEURISTIC_RECOVER_ROLLBACK 2 +#define TC_HEURISTIC_RECOVER_AUTO 3 extern ulong tc_heuristic_recover; typedef struct st_user_var_events