From 984d632454fc19cbba2553288048956dba935df4 Mon Sep 17 00:00:00 2001 From: Hemant Dangi Date: Tue, 31 Mar 2026 18:05:57 +0530 Subject: [PATCH] MDEV-36025: backup taken from a replica with optimistic parallel replication fails to restore most of the time MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Issue: Backups taken from a replica running optimistic parallel replication can restore into a server that aborts on startup with: Found N prepared transactions! ... In the MDEV-36025 reproducer the application never issues XA SQL, but on startup InnoDB reports several transactions “in the XA prepared state” and the server aborts. These internal XA transactions created by optimistic parallel replication on the replica are not covered by MDEV-742 and end up prepared after restore, causing the “Found N prepared transactions” startup failure. This is reproducible by mariabackup.xa_prepared_on_restore testcase, which fails with 'Found N prepared transactions'. Solution: Port the MDEV-21168 fix to MariaDB 10.6. Add SRV_OPERATION_RESTORE_ROLLBACK_XA server operation mode and --rollback-xa option (enabled by default) to mariabackup --prepare. This automatically rolls back prepared XA transactions during prepare, since the backup does not contain the binary log needed to resolve them. Prevent incompatible combination of --rollback_xa and --export options. The combination creates mmap state inconsistency in InnoDB's MTR system, leading to crash. --- extra/mariabackup/xtrabackup.cc | 85 +++++++++++++++++- .../suite/mariabackup/encrypted_export.test | 2 +- .../mariabackup/innodb_force_recovery.test | 6 +- .../mariabackup/innodb_xa_rollback.result | 41 +++++++++ .../suite/mariabackup/innodb_xa_rollback.test | 75 ++++++++++++++++ .../mariabackup/page_compression_level.test | 2 +- mysql-test/suite/mariabackup/partial.test | 4 +- .../suite/mariabackup/partial_exclude.test | 2 +- .../suite/mariabackup/partition_partial.test | 2 +- .../mariabackup/xa_prepared_on_restore.result | 30 +++++++ .../mariabackup/xa_prepared_on_restore.test | 89 +++++++++++++++++++ .../suite/mariabackup/xb_page_compress.test | 2 +- storage/innobase/buf/buf0flu.cc | 3 +- storage/innobase/fil/fil0fil.cc | 9 +- storage/innobase/include/srv0srv.h | 17 ++++ storage/innobase/log/log0recv.cc | 33 +++---- storage/innobase/srv/srv0start.cc | 17 ++-- storage/innobase/trx/trx0rseg.cc | 8 +- storage/innobase/trx/trx0trx.cc | 5 +- 19 files changed, 377 insertions(+), 55 deletions(-) create mode 100644 mysql-test/suite/mariabackup/innodb_xa_rollback.result create mode 100644 mysql-test/suite/mariabackup/innodb_xa_rollback.test create mode 100644 mysql-test/suite/mariabackup/xa_prepared_on_restore.result create mode 100644 mysql-test/suite/mariabackup/xa_prepared_on_restore.test diff --git a/extra/mariabackup/xtrabackup.cc b/extra/mariabackup/xtrabackup.cc index 969ffb6907db0..dae4389d928f9 100644 --- a/extra/mariabackup/xtrabackup.cc +++ b/extra/mariabackup/xtrabackup.cc @@ -79,6 +79,9 @@ Street, Fifth Floor, Boston, MA 02110-1335 USA #include #include #include "trx0sys.h" +#include "trx0trx.h" +#include "trx0roll.h" +#include #include #include #include "ha_innodb.h" @@ -151,6 +154,8 @@ my_bool xtrabackup_help; my_bool xtrabackup_export; my_bool ignored_option; +my_bool xtrabackup_rollback_xa; + longlong xtrabackup_use_memory; uint opt_protocol; @@ -1354,6 +1359,7 @@ enum options_xtrabackup OPT_XTRA_BACKUP, OPT_XTRA_PREPARE, OPT_XTRA_EXPORT, + OPT_XTRA_ROLLBACK_XA, OPT_XTRA_PRINT_PARAM, OPT_XTRA_USE_MEMORY, OPT_XTRA_THROTTLE, @@ -1477,6 +1483,13 @@ struct my_option xb_client_options[]= { "create files to import to another database when prepare.", (G_PTR *) &xtrabackup_export, (G_PTR *) &xtrabackup_export, 0, GET_BOOL, NO_ARG, 0, 0, 0, 0, 0, 0}, + {"rollback-xa", OPT_XTRA_ROLLBACK_XA, + "Rollback prepared XA transactions on --prepare. Enabled by default; " + "use --skip-rollback-xa to disable. " + "After preparing target directory with this option " + "it can no longer be a base for incremental backup.", + (G_PTR *) &xtrabackup_rollback_xa, (G_PTR *) &xtrabackup_rollback_xa, 0, + GET_BOOL, NO_ARG, 1, 0, 0, 0, 0, 0}, {"print-param", OPT_XTRA_PRINT_PARAM, "print parameter of mysqld needed for copyback.", (G_PTR *) &xtrabackup_print_param, (G_PTR *) &xtrabackup_print_param, 0, @@ -6900,8 +6913,26 @@ static bool xtrabackup_prepare_func(char** argv) if (!ok) goto cleanup; } + /* Prevent incompatible combination of --rollback_xa and --export + options. These options cannot be used together because: + - --export sets SRV_OPERATION_RESTORE_EXPORT which makes the redo + log mapping read-only for consistency during table export + - --rollback_xa requires write access to the log system to modify + transaction state during XA rollback + The combination creates mmap state inconsistency in InnoDB's MTR + system, leading to crash. + */ + if (xtrabackup_rollback_xa && xtrabackup_export) { + msg("mariabackup: ERROR: --rollback_xa and --export options cannot " + "be used together. This combination causes internal mmap state " + "inconsistency leading to crashes."); + goto error; + } + srv_operation = xtrabackup_export - ? SRV_OPERATION_RESTORE_EXPORT : SRV_OPERATION_RESTORE; + ? SRV_OPERATION_RESTORE_EXPORT + : (xtrabackup_rollback_xa ? SRV_OPERATION_RESTORE_ROLLBACK_XA + : SRV_OPERATION_RESTORE); if (innodb_init_param()) { goto error; @@ -6920,12 +6951,46 @@ static bool xtrabackup_prepare_func(char** argv) srv_max_dirty_pages_pct_lwm = srv_max_buf_pool_modified_pct; } + if (xtrabackup_rollback_xa) + srv_fast_shutdown = 0; + recv_sys.recovery_on = false; if (innodb_init()) { goto error; } - ut_ad(!fil_system.freeze_space_list); + if (xtrabackup_rollback_xa) { + /* Roll back recovered prepared XA transactions. + The backup does not contain the binary log needed + to resolve them. (MDEV-36025) */ + XID *xid_list = + (XID *) my_malloc(PSI_NOT_INSTRUMENTED, + MAX_XID_LIST_SIZE * sizeof(XID), + MYF(0)); + if (!xid_list) { + msg("Can't allocate memory for XID list"); + ok = false; + goto cleanup; + } + ut_ad(recv_no_log_write); + ut_d(recv_no_log_write = false); + int got; + while ((got = trx_recover_for_mysql(xid_list, + MAX_XID_LIST_SIZE)) > 0) { + for (int i = 0; i < got; i++) { + trx_t *trx = trx_get_trx_by_xid(&xid_list[i]); + if (trx) { + trx_rollback_for_mysql(trx); + trx->free(); + msg("Rolled back prepared XA transaction"); + } + } + } + my_free(xid_list); + ut_d(recv_no_log_write = true); + } + + ut_ad(!fil_system.freeze_space_list); corrupted_pages.read_from_file(MB_CORRUPTED_PAGES_FILE); if (xtrabackup_incremental) @@ -6982,9 +7047,21 @@ static bool xtrabackup_prepare_func(char** argv) else if (ok) xb_write_galera_info(xtrabackup_incremental); #endif - innodb_shutdown(); + /* Without buf_flush_sync(), the rolled-back changes would exist only + in the buffer pool and be lost on shutdown, leaving the data files in + an inconsistent state. + In the innodb_preshutdown(), the condition was updated to include + SRV_OPERATION_RESTORE_ROLLBACK_XA so it waits for transactions when + srv_fast_shutdown == 0. The innodb_preshutdown() is called by + innodb_shutdown(), which will wait for any active transactions to + finish and shut down purge and undo background sources for + SRV_OPERATION_RESTORE_ROLLBACK_XA. */ + if (xtrabackup_rollback_xa) + buf_flush_sync_batch(0, false); + + innodb_shutdown(); - innodb_free_param(); + innodb_free_param(); /* output to metadata file */ if (ok) { diff --git a/mysql-test/suite/mariabackup/encrypted_export.test b/mysql-test/suite/mariabackup/encrypted_export.test index d1802118a0960..7102579de23bc 100644 --- a/mysql-test/suite/mariabackup/encrypted_export.test +++ b/mysql-test/suite/mariabackup/encrypted_export.test @@ -15,7 +15,7 @@ exec $XTRABACKUP --defaults-file=$MYSQLTEST_VARDIR/my.cnf --backup --parallel=10 echo # xtrabackup prepare export; --disable_result_log -exec $XTRABACKUP --prepare --export --target-dir=$targetdir; +exec $XTRABACKUP --prepare --export --target-dir=$targetdir --skip-rollback-xa; --enable_result_log --source include/start_mysqld.inc diff --git a/mysql-test/suite/mariabackup/innodb_force_recovery.test b/mysql-test/suite/mariabackup/innodb_force_recovery.test index 3a7b3c6106c6d..0f37ab90c09db 100644 --- a/mysql-test/suite/mariabackup/innodb_force_recovery.test +++ b/mysql-test/suite/mariabackup/innodb_force_recovery.test @@ -66,7 +66,7 @@ close $fd; EOF --echo # "innodb_force_recovery" should be read from "backup-my.cnf" (mariabackup) --disable_result_log -exec $XTRABACKUP --defaults-file=$targetdir/backup-my.cnf --prepare --export --target-dir=$targetdir >$backuplog; +exec $XTRABACKUP --defaults-file=$targetdir/backup-my.cnf --prepare --export --target-dir=$targetdir --skip-rollback-xa >$backuplog; --enable_result_log --let SEARCH_PATTERN=innodb_force_recovery = 1 --let SEARCH_FILE=$backuplog @@ -84,7 +84,7 @@ close $fd; EOF --echo # "innodb_force_recovery=1" should be read from "backup-my.cnf" (innobackupex) --disable_result_log -exec $XTRABACKUP --innobackupex --defaults-file=$targetdir/backup-my.cnf --apply-log --export $targetdir >$backuplog; +exec $XTRABACKUP --innobackupex --defaults-file=$targetdir/backup-my.cnf --apply-log --export --skip-rollback-xa $targetdir >$backuplog; --enable_result_log --let SEARCH_PATTERN=innodb_force_recovery = 1 --let SEARCH_FILE=$backuplog @@ -121,7 +121,7 @@ close $fd; EOF --echo # "innodb_force_recovery" from the command line should override "backup-my.cnf" (innobackupex) --disable_result_log -exec $XTRABACKUP --innobackupex --defaults-file=$targetdir/backup-my.cnf --apply-log --innodb-force-recovery=0 --export $targetdir >$backuplog; +exec $XTRABACKUP --innobackupex --defaults-file=$targetdir/backup-my.cnf --apply-log --innodb-force-recovery=0 --export --skip-rollback-xa $targetdir >$backuplog; --enable_result_log --let SEARCH_PATTERN=innodb_force_recovery = 1 --let SEARCH_FILE=$backuplog diff --git a/mysql-test/suite/mariabackup/innodb_xa_rollback.result b/mysql-test/suite/mariabackup/innodb_xa_rollback.result new file mode 100644 index 0000000000000..9ba48d54b4a45 --- /dev/null +++ b/mysql-test/suite/mariabackup/innodb_xa_rollback.result @@ -0,0 +1,41 @@ +CALL mtr.add_suppression("Found 1 prepared XA transactions"); +RESET MASTER; +CREATE TABLE t1 (a INT) ENGINE=INNODB; +XA START 'test1'; +INSERT t1 VALUES (10); +XA END 'test1'; +XA PREPARE 'test1'; +XA RECOVER; +formatID gtrid_length bqual_length data +1 5 0 test1 +# xtrabackup backup +XA ROLLBACK 'test1'; +# xtrabackup prepare and rollback prepared XA +# shutdown server +# remove datadir +# xtrabackup move back +# restart +XA RECOVER; +formatID gtrid_length bqual_length data +1 5 0 test1 +# xtrabackup prepare and DO NOT rollback prepared XA +# shutdown server +# remove datadir +# xtrabackup move back +# restart +XA RECOVER; +formatID gtrid_length bqual_length data +1 5 0 test1 +XA ROLLBACK 'test1'; +# xtrabackup prepare for export and rollback prepared XA (should fail) +# The above command should fail with error about incompatible options +# xtrabackup prepare for export and DO NOT rollback prepared XA +# shutdown server +# remove datadir +# xtrabackup move back +# restart +XA RECOVER; +formatID gtrid_length bqual_length data +1 5 0 test1 +XA ROLLBACK 'test1'; +DROP TABLE t1; diff --git a/mysql-test/suite/mariabackup/innodb_xa_rollback.test b/mysql-test/suite/mariabackup/innodb_xa_rollback.test new file mode 100644 index 0000000000000..d86531cc070da --- /dev/null +++ b/mysql-test/suite/mariabackup/innodb_xa_rollback.test @@ -0,0 +1,75 @@ +# +# Optionally rollback prepared XA when backup is prepared +# +--source include/have_innodb.inc +--source include/have_binlog_format_mixed.inc + +CALL mtr.add_suppression("Found 1 prepared XA transactions"); + +RESET MASTER; + +let targetdir1=$MYSQLTEST_VARDIR/tmp/backup1; +let targetdir2=$MYSQLTEST_VARDIR/tmp/backup2; +let targetdir3=$MYSQLTEST_VARDIR/tmp/backup3; +let targetdir4=$MYSQLTEST_VARDIR/tmp/backup4; + +CREATE TABLE t1 (a INT) ENGINE=INNODB; +XA START 'test1'; +INSERT t1 VALUES (10); +XA END 'test1'; +XA PREPARE 'test1'; +XA RECOVER; + +--echo # xtrabackup backup +--disable_result_log +exec $XTRABACKUP --defaults-file=$MYSQLTEST_VARDIR/my.cnf --backup --target-dir=$targetdir1; +--enable_result_log + +perl; +use lib "lib"; +use My::Handles { suppress_init_messages => 1 }; +use My::File::Path; +copytree($ENV{'targetdir1'}, $ENV{'targetdir2'}); +copytree($ENV{'targetdir1'}, $ENV{'targetdir3'}); +copytree($ENV{'targetdir1'}, $ENV{'targetdir4'}); +EOF + +XA ROLLBACK 'test1'; + +--echo # xtrabackup prepare and rollback prepared XA +--disable_result_log +exec $XTRABACKUP --prepare --rollback_xa --target-dir=$targetdir1; +--let $targetdir = $targetdir1 +--source include/restart_and_restore.inc +--enable_result_log +XA RECOVER; + +--echo # xtrabackup prepare and DO NOT rollback prepared XA +--disable_result_log +exec $XTRABACKUP --prepare --target-dir=$targetdir2; +--let $targetdir = $targetdir2 +--source include/restart_and_restore.inc +--enable_result_log +XA RECOVER; +XA ROLLBACK 'test1'; + +--echo # xtrabackup prepare for export and rollback prepared XA (should fail) +--disable_result_log +--error 1 +exec $XTRABACKUP --prepare --rollback_xa --export --target-dir=$targetdir3; +--echo # The above command should fail with error about incompatible options + +--echo # xtrabackup prepare for export and DO NOT rollback prepared XA +--disable_result_log +exec $XTRABACKUP --prepare --export --target-dir=$targetdir4 --skip-rollback-xa; +--let $targetdir = $targetdir4 +--source include/restart_and_restore.inc +--enable_result_log +XA RECOVER; +XA ROLLBACK 'test1'; + +DROP TABLE t1; +rmdir $targetdir1; +rmdir $targetdir2; +rmdir $targetdir3; +rmdir $targetdir4; diff --git a/mysql-test/suite/mariabackup/page_compression_level.test b/mysql-test/suite/mariabackup/page_compression_level.test index e80c956e6210a..2b831877f39f8 100644 --- a/mysql-test/suite/mariabackup/page_compression_level.test +++ b/mysql-test/suite/mariabackup/page_compression_level.test @@ -10,7 +10,7 @@ let $targetdir=$MYSQLTEST_VARDIR/tmp/backup; --disable_result_log exec $XTRABACKUP --defaults-file=$MYSQLTEST_VARDIR/my.cnf --backup --target-dir=$targetdir; echo # xtrabackup prepare; -exec $XTRABACKUP --defaults-file=$MYSQLTEST_VARDIR/my.cnf --defaults-group-suffix=.1 --prepare --export --target-dir=$targetdir; +exec $XTRABACKUP --defaults-file=$MYSQLTEST_VARDIR/my.cnf --defaults-group-suffix=.1 --prepare --export --target-dir=$targetdir --skip-rollback-xa; --enable_result_log --let SEARCH_PATTERN=innodb_compression_level=3 diff --git a/mysql-test/suite/mariabackup/partial.test b/mysql-test/suite/mariabackup/partial.test index e03bd3030c6be..4289395fecb3f 100644 --- a/mysql-test/suite/mariabackup/partial.test +++ b/mysql-test/suite/mariabackup/partial.test @@ -41,7 +41,7 @@ EOF echo # xtrabackup prepare; --disable_result_log -exec $XTRABACKUP --defaults-file=$server_cnf --defaults-group-suffix=.1 --prepare --export --target-dir=$targetdir; +exec $XTRABACKUP --defaults-file=$server_cnf --defaults-group-suffix=.1 --prepare --export --target-dir=$targetdir --skip-rollback-xa; --enable_result_log list_files $targetdir/test *.cfg; @@ -59,7 +59,7 @@ SELECT * FROM t1; --echo # MDEV-33023 Crash in mariadb-backup --prepare --export after --prepare --disable_result_log exec $XTRABACKUP --defaults-file=$server_cnf --defaults-group-suffix=.1 --prepare --target-dir=$targetdir; -exec $XTRABACKUP --defaults-file=$server_cnf --defaults-group-suffix=.1 --prepare --export --target-dir=$targetdir; +exec $XTRABACKUP --defaults-file=$server_cnf --defaults-group-suffix=.1 --prepare --export --target-dir=$targetdir --skip-rollback-xa; --enable_result_log list_files $targetdir/test *.cfg; diff --git a/mysql-test/suite/mariabackup/partial_exclude.test b/mysql-test/suite/mariabackup/partial_exclude.test index fdf113ff5f766..832fde2ce3807 100644 --- a/mysql-test/suite/mariabackup/partial_exclude.test +++ b/mysql-test/suite/mariabackup/partial_exclude.test @@ -57,7 +57,7 @@ rmdir $MYSQLD_DATADIR/db5; --let $backup_log=$MYSQLTEST_VARDIR/tmp/backup.log --disable_result_log -exec $XTRABACKUP --defaults-file=$MYSQLTEST_VARDIR/my.cnf --export --prepare --target-dir="$targetdir" > $backup_log; +exec $XTRABACKUP --defaults-file=$MYSQLTEST_VARDIR/my.cnf --export --prepare --target-dir="$targetdir" --skip-rollback-xa > $backup_log; --enable_result_log --let SEARCH_FILE=$backup_log diff --git a/mysql-test/suite/mariabackup/partition_partial.test b/mysql-test/suite/mariabackup/partition_partial.test index 30e31a9d43e3c..c459d39da685a 100644 --- a/mysql-test/suite/mariabackup/partition_partial.test +++ b/mysql-test/suite/mariabackup/partition_partial.test @@ -23,7 +23,7 @@ INSERT INTO t1 VALUES (1), (101), (201), (301); echo # xtrabackup prepare; --disable_result_log -exec $XTRABACKUP --defaults-file=$MYSQLTEST_VARDIR/my.cnf --defaults-group-suffix=.1 --prepare --export --target-dir=$targetdir; +exec $XTRABACKUP --defaults-file=$MYSQLTEST_VARDIR/my.cnf --defaults-group-suffix=.1 --prepare --export --target-dir=$targetdir --skip-rollback-xa; --enable_result_log diff --git a/mysql-test/suite/mariabackup/xa_prepared_on_restore.result b/mysql-test/suite/mariabackup/xa_prepared_on_restore.result new file mode 100644 index 0000000000000..1c433d7cf4ec6 --- /dev/null +++ b/mysql-test/suite/mariabackup/xa_prepared_on_restore.result @@ -0,0 +1,30 @@ +call mtr.add_suppression("Can't init tc log"); +call mtr.add_suppression("Found .* prepared transactions!"); +call mtr.add_suppression("Aborting"); +CREATE TABLE t1 (a INT) ENGINE=INNODB; +SET GLOBAL innodb_flush_log_at_trx_commit=1; +INSERT INTO t1 VALUES (0); +FLUSH TABLES; +connect con1,localhost,root,,; +SET debug_sync='ha_commit_trans_after_prepare WAIT_FOR go'; +INSERT INTO t1 VALUES (1); +connect con2,localhost,root,,; +SET debug_sync='ha_commit_trans_after_prepare WAIT_FOR go'; +INSERT INTO t1 VALUES (2); +connect con3,localhost,root,,; +SET debug_sync='ha_commit_trans_after_prepare WAIT_FOR go'; +INSERT INTO t1 VALUES (3); +connect con4,localhost,root,,; +SET debug_sync='ha_commit_trans_after_prepare WAIT_FOR go'; +INSERT INTO t1 VALUES (4); +connect con5,localhost,root,,; +SET debug_sync='ha_commit_trans_after_prepare WAIT_FOR go'; +INSERT INTO t1 VALUES (5); +connection default; +# Kill the server +FOUND 1 /Found .* prepared transactions!/ in mysqld.1.err +# restart +SELECT * FROM t1; +a +0 +DROP TABLE t1; diff --git a/mysql-test/suite/mariabackup/xa_prepared_on_restore.test b/mysql-test/suite/mariabackup/xa_prepared_on_restore.test new file mode 100644 index 0000000000000..3f5935fdc372e --- /dev/null +++ b/mysql-test/suite/mariabackup/xa_prepared_on_restore.test @@ -0,0 +1,89 @@ +# +# MDEV-36025: After mariabackup --prepare, server startup fails with +# "Found N prepared transactions!" because the backup does not include +# the binary log needed to resolve internal 2PC prepared transactions. +# + +--source include/have_innodb.inc +--source include/have_log_bin.inc +--source include/have_debug_sync.inc +--source include/not_embedded.inc + +call mtr.add_suppression("Can't init tc log"); +call mtr.add_suppression("Found .* prepared transactions!"); +call mtr.add_suppression("Aborting"); + +CREATE TABLE t1 (a INT) ENGINE=INNODB; +SET GLOBAL innodb_flush_log_at_trx_commit=1; +INSERT INTO t1 VALUES (0); +FLUSH TABLES; + +let $_datadir= `SELECT @@datadir`; +let $targetdir=$MYSQLTEST_VARDIR/tmp/backup; +let SEARCH_FILE= $MYSQLTEST_VARDIR/log/mysqld.1.err; + +# Pause 5 transactions after internal 2PC prepare (InnoDB prepare done, +# but not yet committed in binlog). + +--connect (con1,localhost,root,,) +SET debug_sync='ha_commit_trans_after_prepare WAIT_FOR go'; +--send INSERT INTO t1 VALUES (1) + +--connect (con2,localhost,root,,) +SET debug_sync='ha_commit_trans_after_prepare WAIT_FOR go'; +--send INSERT INTO t1 VALUES (2) + +--connect (con3,localhost,root,,) +SET debug_sync='ha_commit_trans_after_prepare WAIT_FOR go'; +--send INSERT INTO t1 VALUES (3) + +--connect (con4,localhost,root,,) +SET debug_sync='ha_commit_trans_after_prepare WAIT_FOR go'; +--send INSERT INTO t1 VALUES (4) + +--connect (con5,localhost,root,,) +SET debug_sync='ha_commit_trans_after_prepare WAIT_FOR go'; +--send INSERT INTO t1 VALUES (5) + +--connection default + +--let $wait_condition= SELECT count(*) = 5 FROM information_schema.processlist WHERE state = 'debug sync point: ha_commit_trans_after_prepare' +--source include/wait_condition.inc + +# xtrabackup backup with --no-lock to avoid deadlock with paused transaction; +--disable_result_log +exec $XTRABACKUP --defaults-file=$MYSQLTEST_VARDIR/my.cnf --backup --no-lock --target-dir=$targetdir; +--enable_result_log + +# Kill server while transaction is still in prepared state +--source include/kill_mysqld.inc + +# xtrabackup prepare; +--disable_result_log +exec $XTRABACKUP --prepare --target-dir=$targetdir; +--enable_result_log + +# Restore from backup +rmdir $_datadir; +--exec $XTRABACKUP --defaults-file=$MYSQLTEST_VARDIR/my.cnf --copy-back --datadir=$_datadir --target-dir=$targetdir --parallel=2 --throttle=1 + +# Try to start the server - should fail because InnoDB has prepared +# transactions but there is no binlog to resolve them. +--error 1 +--exec $MYSQLD_LAST_CMD + +--let SEARCH_PATTERN= Found .* prepared transactions! +--source include/search_pattern_in_file.inc + +# Use --tc-heuristic-recover=ROLLBACK to roll back the prepared transactions +--error 1 +--exec $MYSQLD_LAST_CMD --tc-heuristic-recover=ROLLBACK + +# Normal start should now succeed +--source include/start_mysqld.inc + +# After restore, prepared transactions should have been rolled back; +SELECT * FROM t1; + +DROP TABLE t1; +rmdir $targetdir; diff --git a/mysql-test/suite/mariabackup/xb_page_compress.test b/mysql-test/suite/mariabackup/xb_page_compress.test index e2819e264c1a2..5b761b7e86e91 100644 --- a/mysql-test/suite/mariabackup/xb_page_compress.test +++ b/mysql-test/suite/mariabackup/xb_page_compress.test @@ -29,7 +29,7 @@ let $targetdir=$MYSQLTEST_VARDIR/tmp/backup; --disable_result_log exec $XTRABACKUP --defaults-file=$MYSQLTEST_VARDIR/my.cnf --backup --parallel=10 "--tables=test.*1" --target-dir=$targetdir; echo # xtrabackup prepare; -exec $XTRABACKUP --defaults-file=$MYSQLTEST_VARDIR/my.cnf --defaults-group-suffix=.1 --prepare --export --target-dir=$targetdir; +exec $XTRABACKUP --defaults-file=$MYSQLTEST_VARDIR/my.cnf --defaults-group-suffix=.1 --prepare --export --target-dir=$targetdir --skip-rollback-xa; --enable_result_log ALTER TABLE t1 DISCARD TABLESPACE; diff --git a/storage/innobase/buf/buf0flu.cc b/storage/innobase/buf/buf0flu.cc index e1d153b0cc9aa..17e189e0e06d3 100644 --- a/storage/innobase/buf/buf0flu.cc +++ b/storage/innobase/buf/buf0flu.cc @@ -2769,8 +2769,7 @@ ATTRIBUTE_COLD void buf_flush_page_cleaner_init() noexcept { ut_ad(!buf_page_cleaner_is_active); ut_ad(srv_operation <= SRV_OPERATION_EXPORT_RESTORED || - srv_operation == SRV_OPERATION_RESTORE || - srv_operation == SRV_OPERATION_RESTORE_EXPORT); + is_mariabackup_restore_or_export()); buf_flush_async_lsn= 0; buf_flush_sync_lsn= 0; buf_page_cleaner_is_active= true; diff --git a/storage/innobase/fil/fil0fil.cc b/storage/innobase/fil/fil0fil.cc index 1b9dd6228aacc..7d45258469422 100644 --- a/storage/innobase/fil/fil0fil.cc +++ b/storage/innobase/fil/fil0fil.cc @@ -437,7 +437,7 @@ static bool fil_node_open_file(fil_node_t *node, const byte *page, bool no_lsn) ut_ad(!node->is_open()); ut_ad(!is_predefined_tablespace(node->space->id) || srv_operation == SRV_OPERATION_BACKUP || - srv_operation == SRV_OPERATION_RESTORE || + is_mariabackup_restore() || srv_operation == SRV_OPERATION_RESTORE_DELTA); ut_ad(!node->space->is_temporary()); ut_ad(node->space->referenced()); @@ -1172,8 +1172,8 @@ void fil_space_t::close() mysql_mutex_lock(&fil_system.mutex); ut_ad(this == fil_system.temp_space || srv_operation == SRV_OPERATION_BACKUP - || srv_operation == SRV_OPERATION_RESTORE - || srv_operation == SRV_OPERATION_RESTORE_DELTA); + || srv_operation == SRV_OPERATION_RESTORE_DELTA + || is_mariabackup_restore()); for (fil_node_t* node = UT_LIST_GET_FIRST(chain); node != NULL; @@ -2322,6 +2322,7 @@ fil_ibd_discover( break; case SRV_OPERATION_RESTORE_EXPORT: case SRV_OPERATION_RESTORE: + case SRV_OPERATION_RESTORE_ROLLBACK_XA: break; case SRV_OPERATION_NORMAL: case SRV_OPERATION_EXPORT_RESTORED: @@ -2426,7 +2427,7 @@ fil_ibd_load(uint32_t space_id, const char *filename, fil_space_t *&space) noexc return FIL_LOAD_ID_CHANGED; } - if (srv_operation == SRV_OPERATION_RESTORE) { + if (is_mariabackup_restore()) { /* Replace absolute DATA DIRECTORY file paths with short names relative to the backup directory. */ const char* name = strrchr(filename, '/'); diff --git a/storage/innobase/include/srv0srv.h b/storage/innobase/include/srv0srv.h index fafb6218002bd..bae9f1440b7b2 100644 --- a/storage/innobase/include/srv0srv.h +++ b/storage/innobase/include/srv0srv.h @@ -322,6 +322,8 @@ enum srv_operation_mode { SRV_OPERATION_BACKUP, /** Mariabackup restoring a backup for subsequent --copy-back */ SRV_OPERATION_RESTORE, + /** Mariabackup restoring a backup with rolling back prepared XA's */ + SRV_OPERATION_RESTORE_ROLLBACK_XA, /** Mariabackup restoring the incremental part of a backup */ SRV_OPERATION_RESTORE_DELTA, /** Mariabackup restoring a backup for subsequent --export */ @@ -334,6 +336,21 @@ enum srv_operation_mode { /** Current mode of operation */ extern enum srv_operation_mode srv_operation; +inline bool is_mariabackup_restore() +{ + /* To rollback XA's trx_sys must be initialized, the rest is the same + as regular backup restore, that is why we join these two operations in + most cases. */ + return srv_operation == SRV_OPERATION_RESTORE + || srv_operation == SRV_OPERATION_RESTORE_ROLLBACK_XA; +} + +inline bool is_mariabackup_restore_or_export() +{ + return is_mariabackup_restore() + || srv_operation == SRV_OPERATION_RESTORE_EXPORT; +} + /** whether this is the server's first start after mariabackup --prepare */ extern bool srv_start_after_restore; diff --git a/storage/innobase/log/log0recv.cc b/storage/innobase/log/log0recv.cc index 3d87d274b2844..9e454eac2a3ce 100644 --- a/storage/innobase/log/log0recv.cc +++ b/storage/innobase/log/log0recv.cc @@ -717,7 +717,7 @@ static struct mysql_mutex_assert_owner(&recv_sys.mutex); const char *filename= f_name.c_str(); - if (srv_operation == SRV_OPERATION_RESTORE) + if (is_mariabackup_restore()) { /* Replace absolute DATA DIRECTORY file paths with short names relative to the backup directory. */ @@ -894,7 +894,7 @@ static struct crypt_data); ut_ad(space); const char *filename= name.c_str(); - if (srv_operation == SRV_OPERATION_RESTORE) + if (is_mariabackup_restore()) { if (const char *tbl_name= strrchr(filename, '/')) { @@ -904,7 +904,7 @@ static struct } } pfs_os_file_t handle= OS_FILE_CLOSED; - if (srv_operation == SRV_OPERATION_RESTORE) + if (is_mariabackup_restore()) { /* During mariadb-backup --backup, a table could be renamed, created and dropped, and we may be missing the file at this @@ -1308,8 +1308,7 @@ static void fil_name_process(const char *name, ulint len, uint32_t space_id, mfile_type_t ftype, lsn_t lsn, bool if_exists) { ut_ad(srv_operation <= SRV_OPERATION_EXPORT_RESTORED - || srv_operation == SRV_OPERATION_RESTORE - || srv_operation == SRV_OPERATION_RESTORE_EXPORT); + || is_mariabackup_restore_or_export()); /* We will also insert space=NULL into the map, so that further checks can ensure that a FILE_MODIFY record was @@ -1405,7 +1404,7 @@ static void fil_name_process(const char *name, ulint len, uint32_t space_id, FILE_* record. */ ut_ad(space == NULL); - if (srv_operation == SRV_OPERATION_RESTORE && d + if (is_mariabackup_restore() && d && ftype == FILE_RENAME) { rename: d->file_name = fname.name; @@ -1418,7 +1417,7 @@ static void fil_name_process(const char *name, ulint len, uint32_t space_id, } if (srv_force_recovery - || srv_operation == SRV_OPERATION_RESTORE) { + || is_mariabackup_restore()) { /* Without innodb_force_recovery, missing tablespaces will only be reported in @@ -1438,7 +1437,7 @@ static void fil_name_process(const char *name, ulint len, uint32_t space_id, case FIL_LOAD_DEFER: if (d && ftype == FILE_RENAME - && srv_operation == SRV_OPERATION_RESTORE) { + && is_mariabackup_restore()) { goto rename; } /* Skip the deferred spaces @@ -4113,8 +4112,7 @@ ATTRIBUTE_COLD static void buf_pool_invalidate() noexcept void recv_sys_t::apply(bool last_batch) { ut_ad(srv_operation <= SRV_OPERATION_EXPORT_RESTORED || - srv_operation == SRV_OPERATION_RESTORE || - srv_operation == SRV_OPERATION_RESTORE_EXPORT); + is_mariabackup_restore_or_export()); mysql_mutex_assert_owner(&mutex); @@ -4147,9 +4145,7 @@ void recv_sys_t::apply(bool last_batch) if (!pages.empty()) { recv_no_ibuf_operations = !last_batch || - srv_operation == SRV_OPERATION_RESTORE || - srv_operation == SRV_OPERATION_RESTORE_EXPORT; - ut_ad(!last_batch || lsn == scanned_lsn); + is_mariabackup_restore_or_export(); progress_time= time(nullptr); report_progress(); @@ -4237,8 +4233,7 @@ void recv_sys_t::apply(bool last_batch) buf_pool_invalidate(); log_sys.latch.wr_lock(); } - else if (srv_operation == SRV_OPERATION_RESTORE || - srv_operation == SRV_OPERATION_RESTORE_EXPORT) + else if (is_mariabackup_restore_or_export()) buf_flush_sync_batch(lsn, false); else /* Instead of flushing, last_batch sorts the buf_pool.flush_list @@ -4488,6 +4483,7 @@ recv_init_missing_space(dberr_t err, const recv_spaces_t::const_iterator& i) default: break; case SRV_OPERATION_RESTORE: + case SRV_OPERATION_RESTORE_ROLLBACK_XA: case SRV_OPERATION_RESTORE_EXPORT: if (i->second.name.find("/#sql") == std::string::npos) { sql_print_warning("InnoDB: Tablespace " UINT32PF @@ -4820,8 +4816,7 @@ dberr_t recv_recovery_from_checkpoint_start() dberr_t err = DB_SUCCESS; ut_ad(srv_operation <= SRV_OPERATION_EXPORT_RESTORED - || srv_operation == SRV_OPERATION_RESTORE - || srv_operation == SRV_OPERATION_RESTORE_EXPORT); + || is_mariabackup_restore_or_export()); ut_d(mysql_mutex_lock(&buf_pool.flush_list_mutex)); ut_ad(UT_LIST_GET_LEN(buf_pool.LRU) == 0); ut_ad(UT_LIST_GET_LEN(buf_pool.unzip_LRU) == 0); @@ -4989,8 +4984,8 @@ dberr_t recv_recovery_from_checkpoint_start() } recv_sys.apply_log_recs = true; recv_no_ibuf_operations = false; - ut_d(recv_no_log_write = srv_operation == SRV_OPERATION_RESTORE - || srv_operation == SRV_OPERATION_RESTORE_EXPORT); + ut_d(recv_no_log_write = is_mariabackup_restore_or_export()); + if (srv_operation == SRV_OPERATION_NORMAL) { err = recv_rename_files(); } diff --git a/storage/innobase/srv/srv0start.cc b/storage/innobase/srv/srv0start.cc index 397656fc0e59e..4c97676a2c011 100644 --- a/storage/innobase/srv/srv0start.cc +++ b/storage/innobase/srv/srv0start.cc @@ -1128,8 +1128,7 @@ dberr_t srv_start(bool create_new_db) mtr_t mtr; ut_ad(srv_operation <= SRV_OPERATION_RESTORE_EXPORT - || srv_operation == SRV_OPERATION_RESTORE - || srv_operation == SRV_OPERATION_RESTORE_EXPORT); + || is_mariabackup_restore_or_export()); if (srv_force_recovery) { ib::info() << "!!! innodb_force_recovery is set to " @@ -1289,8 +1288,7 @@ dberr_t srv_start(bool create_new_db) recv_sys.debug_free(); } else { ut_ad(srv_operation <= SRV_OPERATION_EXPORT_RESTORED - || srv_operation == SRV_OPERATION_RESTORE - || srv_operation == SRV_OPERATION_RESTORE_EXPORT); + || is_mariabackup_restore_or_export()); ut_ad(!recv_sys.recovery_on); if (srv_force_recovery >= SRV_FORCE_NO_LOG_REDO) { @@ -1451,6 +1449,7 @@ dberr_t srv_start(bool create_new_db) switch (srv_operation) { case SRV_OPERATION_NORMAL: case SRV_OPERATION_EXPORT_RESTORED: + case SRV_OPERATION_RESTORE_ROLLBACK_XA: case SRV_OPERATION_RESTORE_EXPORT: /* Initialize the change buffer. */ err = dict_boot(); @@ -1462,8 +1461,7 @@ dberr_t srv_start(bool create_new_db) /* This must precede recv_sys.apply(true). */ srv_undo_tablespaces_active = trx_rseg_get_n_undo_tablespaces(); - - if (srv_operation != SRV_OPERATION_RESTORE) { + if (!is_mariabackup_restore()) { dict_sys.load_sys_tables(); } err = trx_lists_init_at_db_start(); @@ -1508,8 +1506,7 @@ dberr_t srv_start(bool create_new_db) fil_system.space_id_reuse_warned = false; if (srv_operation > SRV_OPERATION_EXPORT_RESTORED) { - ut_ad(srv_operation == SRV_OPERATION_RESTORE_EXPORT - || srv_operation == SRV_OPERATION_RESTORE); + ut_ad(is_mariabackup_restore_or_export()); return(err); } @@ -1915,7 +1912,9 @@ void innodb_preshutdown() if (srv_read_only_mode) return; - if (!srv_fast_shutdown && srv_operation <= SRV_OPERATION_EXPORT_RESTORED) + if (!srv_fast_shutdown && (srv_operation <= SRV_OPERATION_EXPORT_RESTORED + || srv_operation + == SRV_OPERATION_RESTORE_ROLLBACK_XA)) { /* Because a slow shutdown must empty the change buffer, we had better prevent any further changes from being buffered. */ diff --git a/storage/innobase/trx/trx0rseg.cc b/storage/innobase/trx/trx0rseg.cc index 34a371c50c957..8381e11bb1ddc 100644 --- a/storage/innobase/trx/trx0rseg.cc +++ b/storage/innobase/trx/trx0rseg.cc @@ -519,7 +519,7 @@ static dberr_t trx_rseg_mem_restore(trx_rseg_t *rseg, mtr_t *mtr) #endif } - if (srv_operation == SRV_OPERATION_RESTORE) + if (is_mariabackup_restore()) /* mariabackup --prepare only deals with the redo log and the data files, not with transactions or the data dictionary. */ @@ -615,11 +615,11 @@ dberr_t trx_rseg_array_init() trx_lists_init_at_db_start() does not invoke purge_sys.create() and purge queue mutex stays uninitialized, and trx_rseg_mem_restore() quits before initializing undo log lists. */ - if (srv_operation != SRV_OPERATION_RESTORE) + if (!is_mariabackup_restore()) /* Acquiring purge queue mutex here should be fine from the deadlock prevention point of view, because executing that function is a prerequisite for starting the purge subsystem or - any transactions. */ + trx_rseg_mem_restore() quits before initializing undo log lists. */ purge_sys.queue_lock(); for (ulint rseg_id = 0; rseg_id < TRX_SYS_N_RSEGS; rseg_id++) { mtr.start(); @@ -691,7 +691,7 @@ dberr_t trx_rseg_array_init() mtr.commit(); } - if (srv_operation != SRV_OPERATION_RESTORE) + if (!is_mariabackup_restore()) purge_sys.queue_unlock(); if (err != DB_SUCCESS) { for (auto& rseg : trx_sys.rseg_array) { diff --git a/storage/innobase/trx/trx0trx.cc b/storage/innobase/trx/trx0trx.cc index 1ff3b9ac36d4a..79c558b41e9a4 100644 --- a/storage/innobase/trx/trx0trx.cc +++ b/storage/innobase/trx/trx0trx.cc @@ -510,8 +510,7 @@ TRANSACTIONAL_TARGET void trx_free_at_shutdown(trx_t *trx) || trx_state_eq(trx, TRX_STATE_PREPARED_RECOVERED) || (trx_state_eq(trx, TRX_STATE_ACTIVE) && (!srv_was_started - || srv_operation == SRV_OPERATION_RESTORE - || srv_operation == SRV_OPERATION_RESTORE_EXPORT + || is_mariabackup_restore_or_export() || srv_read_only_mode || srv_force_recovery >= SRV_FORCE_NO_TRX_UNDO || (!srv_is_being_started @@ -701,7 +700,7 @@ dberr_t trx_lists_init_at_db_start() ut_a(srv_is_being_started); ut_ad(!srv_was_started); - if (srv_operation == SRV_OPERATION_RESTORE) { + if (is_mariabackup_restore()) { /* mariabackup --prepare only deals with the redo log and the data files, not with transactions or the data dictionary. */