commit 99438c89f5d6d2a503673833ab406e9920197d5f Author: Libing Song Date: Tue Mar 28 07:56:58 2023 +0800 BUG#110430 XA COMMIT may not persist GTID into undo header Analysis ======== After the server recovered from a crash, XA COMMIT could not persist GTID into undo with an error “[InnoDB] Could not persist GTID as space for GTID is not allocated.“, if the crash just happened before set PREPARED_IN_TC flag. It caused an assertion failure in debug mode. When allocating first update undo header, it will reserve space for GTIDs and undo->m_gtid_storage is set to tell XA PREPARE or XA COMMIT that space is reserved for GTIDS. Thus they can write GTIDs into undo log header. But undo->m_gtid_storage was not initialized correctly after server restart. undo->m_gtid_storage was recovered by checking gtid flags(TRX_UNDO_FLAG_XA_PREPARE_GTID, TRX_UNDO_FLAG_GTID) in the undo header. However, the flags is written with a GTID together which happens after undo header is initialized. Crash could happen between them. Fix === If there is no any gtid flag in undo header, it will check undo header's size to know if space is reserved for GTIDs. diff --git a/mysql-test/suite/binlog_gtid/r/binlog_gtid_xa_prepare_failure_before_prepare_2nd_phase.result b/mysql-test/suite/binlog_gtid/r/binlog_gtid_xa_prepare_failure_before_prepare_2nd_phase.result index 9b4b4a59d69..935ef561df4 100644 --- a/mysql-test/suite/binlog_gtid/r/binlog_gtid_xa_prepare_failure_before_prepare_2nd_phase.result +++ b/mysql-test/suite/binlog_gtid/r/binlog_gtid_xa_prepare_failure_before_prepare_2nd_phase.result @@ -27,3 +27,41 @@ include/assert.inc [1 XA transaction(s) in PREPARED state] include/assert.inc [XA transaction X'78696431',X'',1 is in PREPARED state] XA ROLLBACK X'78696431',X'',1; DROP TABLE t1; +# +# BUG#110430 XA COMMIT may not persist GTID into undo header +# +# It repeats above test with an extra DELETE statement to generate an +# update undo befor crash and verifys the transaction can be committed +# successfully after restart. +######################################################################### +include/suppress_messages.inc +# Connection 1 suppresses message . +# Connection 1 suppresses message <.*Checksum mismatch in datafile.*>. +CREATE TABLE t1 (c1 INT PRIMARY KEY); +XA START X'78696432',X'',1; +INSERT INTO t1 VALUES (2); +XA END X'78696432',X'',1; +XA PREPARE X'78696432',X'',1; +XA COMMIT X'78696432',X'',1; +include/save_binlog_position.inc +XA START X'78696432',X'',1; +INSERT INTO t1 VALUES (1); +DELETE FROM t1; +XA END X'78696432',X'',1; +include/execute_to_conditional_timestamp_sync_point.inc [before_set_prepared_in_tc] +XA PREPARE X'78696432',X'',1; +# Kill the server +ERROR HY000: Lost connection to MySQL server during query +# restart +include/assert.inc [Found 1 log message(s) for "Successfully prepared 1 XA transaction"] +include/assert_binlog_events.inc [Gtid # Query/XA START X'78696432',X'',1 # Table_map # Write_rows # Table_map # Delete_rows # Query/XA END X'78696432',X'',1 # XA_prepare/XA PREPARE X'78696432',X'',1(#.*)*] +include/assert.inc [GTID_EXECUTED has been updated] +include/assert.inc [1 XA transaction(s) in PREPARED state] +include/assert.inc [XA transaction X'78696432',X'',1 is in PREPARED state] +include/assert.inc [Table 'test.t1' record count must be 1] +# restart +include/assert.inc [1 XA transaction(s) in PREPARED state] +include/assert.inc [XA transaction X'78696432',X'',1 is in PREPARED state] +XA COMMIT X'78696432',X'',1; +include/assert_grep.inc [Not found error 'Could not persist GTID'] +DROP TABLE t1; diff --git a/mysql-test/suite/binlog_gtid/t/binlog_gtid_xa_prepare_failure_before_prepare_2nd_phase.test b/mysql-test/suite/binlog_gtid/t/binlog_gtid_xa_prepare_failure_before_prepare_2nd_phase.test index 902d01db5c7..eb030e2dd5c 100644 --- a/mysql-test/suite/binlog_gtid/t/binlog_gtid_xa_prepare_failure_before_prepare_2nd_phase.test +++ b/mysql-test/suite/binlog_gtid/t/binlog_gtid_xa_prepare_failure_before_prepare_2nd_phase.test @@ -120,3 +120,97 @@ INSERT INTO t1 VALUES (1); --eval XA ROLLBACK $xid DROP TABLE t1; + +--echo # +--echo # BUG#110430 XA COMMIT may not persist GTID into undo header +--echo # +--echo # It repeats above test with an extra DELETE statement to generate an +--echo # update undo befor crash and verifys the transaction can be committed +--echo # successfully after restart. +--echo ######################################################################### + +# +# 1. Setup scenario: create table and insert some records. +# +--let $xid_data = xid2 +--let $xid = `SELECT CONCAT("X'", LOWER(HEX('$xid_data')), "',X'',1")` +--source extra/xa_crash_safe_tests/setup.inc + +# 2. Start and execute an XA transaction containing an insert until before +# `XA PREPARE`. +# +--connect(con1, localhost, root,,) +--connection con1 +--eval XA START $xid +INSERT INTO t1 VALUES (1); +DELETE FROM t1; +--eval XA END $xid + +# 3. Take the `GTID_EXECUTED` state. +# +--let $before_gtid_executed = `SELECT @@GLOBAL.gtid_executed` + +# 4. Crash the server during `XA PREPARE` execution, after writing to the +# binlog and before marking the transaction as prepared in TC. +# +--let $auxiliary_connection = default +--let $statement_connection = con1 +--let $statement = XA PREPARE $xid +--let $sync_point = before_set_prepared_in_tc +--source include/execute_to_conditional_timestamp_sync_point.inc +--source include/kill_mysqld.inc +--source extra/xa_crash_safe_tests/cleanup_connection.inc + +# 5. Restart server and check: +# +--source include/start_mysqld.inc + +# 5.a. Error log for messages stating that recovery process found one +# transaction needing recovery. +# +--let $assert_select = Successfully prepared 1 XA transaction +--source extra/xa_crash_safe_tests/assert_recovery_message.inc + +# 5.b. The `XA PREPARE` was logged to the binary log. +# +--let $xa_start_end = Query/XA START $xid # Table_map # Write_rows # Table_map # Delete_rows # Query/XA END $xid +--let $event_sequence = Gtid # $xa_start_end # XA_prepare/XA PREPARE $xid(#.*)* +--source include/assert_binlog_events.inc + +# 5.c. The GTID_EXECUTED variable was updated. +# +--let $after_gtid_executed = `SELECT @@GLOBAL.gtid_executed` +--let $assert_text = GTID_EXECUTED has been updated +--let $assert_cond = "$before_gtid_executed" != "$after_gtid_executed" +--source include/assert.inc + +# 5.d. There is one pending XA transaction listed in the output of `XA +# RECOVER`. +# +--let $expected_prepared_xa_count = 1 +--source extra/xa_crash_safe_tests/assert_xa_recover.inc + +# 5.e. There aren't changes to the table. +# +--let $expected_row_count = 1 +--source extra/xa_crash_safe_tests/assert_row_count.inc + +# 6. Restart the server and check that the transaction is still in preared +# state, meaning, the recovery process moved the transaction to +# `PERSISTED_IN_TC` and that state was persisted. +# +--source include/restart_mysqld.inc +--let $expected_prepared_xa_count = 1 +--source extra/xa_crash_safe_tests/assert_xa_recover.inc + +# 7. Commit the transaction and check no error logged. +--eval XA COMMIT $xid + +--let $assert_file = $MYSQLTEST_VARDIR/log/mysqld.1.err +--let $assert_text = Not found error 'Could not persist GTID' +--let $assert_select = Could not persist GTID as space for GTID is not allocated +--let $assert_count = 0 +--let $assert_only_after = CURRENT_TEST: binlog_gtid.binlog_gtid_xa_prepare_failure_before_prepare_2nd_phase +--source include/assert_grep.inc + +DROP TABLE t1; diff --git a/storage/innobase/trx/trx0undo.cc b/storage/innobase/trx/trx0undo.cc index 0147a5a0925..03d201e3824 100644 --- a/storage/innobase/trx/trx0undo.cc +++ b/storage/innobase/trx/trx0undo.cc @@ -1343,6 +1343,18 @@ static trx_undo_t *trx_undo_mem_init( } else if ((flag & TRX_UNDO_FLAG_GTID) != 0) { /* Space is allocated for only commit GTID. */ undo->m_gtid_storage = trx_undo_t::Gtid_storage::COMMIT; + } else if (type == TRX_UNDO_UPDATE) { + ulint start = mach_read_from_2(undo_header + TRX_UNDO_LOG_START); + + /* Update undo has allocated space for GTIDs but no any GTID was not written + yet, so no GTID flag was set. Thus it checks the header's size to know + whether space is allocated for GTIDs. + */ + if (start - offset == TRX_UNDO_LOG_GTID_XA_HDR_SIZE) { + undo->m_gtid_storage = trx_undo_t::Gtid_storage::PREPARE_AND_COMMIT; + } else if (start - offset == TRX_UNDO_LOG_GTID_HDR_SIZE) { + undo->m_gtid_storage = trx_undo_t::Gtid_storage::COMMIT; + } } undo->state = state;