Refuse XA workloads in the binlog stream instead of applying prepared rows - #1079
Conversation
… rows Both change clients treated an XA transaction's prepare-time row events as committed: the row images are written to the binary log at XA PREPARE time and were buffered and flushed to the target like any commit. If the transaction was later terminated with XA ROLLBACK, nothing in the binlog undoes those rows, so the target diverged permanently - detectable only by checksum, and only if the affected chunk was checksummed after the rollback. Full XA support (tracking prepared XIDs and buffering until the XA COMMIT / XA ROLLBACK outcome) is out of scope, so fail fast instead, matching the existing posture of refusing binlog_row_value_options: any XA statement (XA START / XA END / XA COMMIT / XA ROLLBACK QueryEvents) or XA_PREPARE_LOG_EVENT observed in the stream fails the migration with "XA transactions detected in the binlog stream: spirit does not support XA workloads", surfaced through the existing fatal stream-error path (checkpoint preserved). The guard fires at the group's opening "XA START" QueryEvent, which the server writes ahead of the transaction's row events, so none of them are ever buffered - a concurrent flush therefore cannot apply them, and the resume coordinate never advances past the refused group. Applies to both the GTID and file/offset clients, for uncompressed streams and binlog_transaction_compression payloads alike. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds runtime protection to Spirit’s binlog change stream so that any XA activity causes a fail-fast, checkpoint-preserving abort, preventing permanent target divergence from prepare-time row events being applied before an XA outcome is known.
Changes:
- Introduce a shared XA detection helper/error and use it to abort both change clients on XA QueryEvents and
XA_PREPARE_LOG_EVENT(including withinbinlog_transaction_compressionpayloads). - Update GTID client query-event processing to return fatal errors to the stream reader (instead of silently continuing).
- Add/adjust end-to-end and synthetic-stream tests ensuring the abort happens before buffering/applying any XA row events and that resume coordinates don’t advance past refused groups.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| pkg/change/utils.go | Adds shared XA guard error and helper used by both change clients. |
| pkg/change/gtid.go | GTID client: fail stream on XA QueryEvents / XA prepare event (incl. compressed payload path) and plumb fatal errors up to the reader loop. |
| pkg/change/gtid_test.go | Expands GTID tests to assert fail-fast behavior, empty buffers, preserved checkpoint semantics, and compressed-stream coverage. |
| pkg/change/binlog.go | Binlog (file/offset) client: add the same XA guards in both uncompressed and compressed event paths. |
| pkg/change/binlog_test.go | Adds binlog client tests mirroring GTID coverage for fail-fast XA handling and compressed payload guarding. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
🤖 Adversarial correctness review. I reviewed all nine files at 1 blocking, 4 non-blocking. The XA guard does what it says, and the tests pin it (below). The blocking finding is outside XA. Folding the query-event classification into one parse moved The XA guards are pinned: 4 of 5 mutations are caught, and the fifth is non-blocking finding 2
Blocking1. The GTID client no longer raises a DDL notification for Since 8.0.21, MySQL binlogs // origin/main
if !opensTransaction {
c.promotePendingGTID()
}
for _, ddlTable := range ddlTables {
c.processDDLNotification(ddlTable.schema, ddlTable.table)
}At head, the same flag now returns before the notification loop ( if info.opensTransaction || info.keepsTransactionOpen {
return nil
}Two callers depend on that notification. The binlog client did not change here: its loop over Nothing in the suite catches this. Test that fails on
|
|
🤖 Adversarial correctness review on Jash’s behalf at Verdict: the XA refusal addresses prepared-row divergence, but the existing CTAS notification regression remains blocking. 1. Blocking — preserve DDL notifications for transaction-opening CREATE TABLE events. I independently reproduced the issue in the existing review:
Verified: reviewed all nine changed files and the surrounding stream, payload, GTID promotion, DDL-filter, and runner cancellation paths; XA query rejection precedes row dispatch in both clients, compressed errors propagate to the fatal path, and terminal XA events are refused. The replaced XA-success assertions are superseded by refusal/zero-buffer assertions. Locally, Reviewed by Codex (GPT-6). |
aparajon
left a comment
There was a problem hiding this comment.
🤖 Stamped: 1 blocking, 4 non-blocking. See the review comment: #1079 (comment). The blocking finding is that the GTID client drops the CTAS DDL notification; it still needs fixing before merge.
This stamp was left by Claude Code (claude-opus-5-5).
Why
MySQL writes row events for an XA transaction when it is prepared, before it knows whether the transaction will commit. Spirit could copy those rows to the target and leave them there after
XA ROLLBACK. That can cause permanent data divergence.What
Spirit now stops a migration when either binlog client sees an XA statement or
XA_PREPARE_LOG_EVENT. This covers plain and compressed binlog events, including anXA COMMITorXA ROLLBACKfor a transaction prepared before Spirit connected.How
Both clients parse each QueryEvent once. The parser's XA and transaction-control nodes determine whether to stop, keep a GTID pending, promote it, or notify a DDL subscriber. A migration fails on
XA START, before that transaction's row events can enter a subscription buffer. Finite runs discard the checkpoint because replay would hit the same refused group.XA tests run in their own GitHub Actions job with a separate MySQL server. The regular test jobs skip those tests. This prevents the XA events from interrupting other tests that share a server.
Risk
Any XA activity on the source server now stops the migration, including activity on an unrelated table. This is intentional while the binlog clients cannot safely apply prepared rows. Stop XA activity before starting a fresh migration or move. Datasync reports the distinct
unsupported-xareason.Testing
No manual testing. GitHub Actions runs the MySQL integration suites, the isolated XA suite, and lint.
Generated with Codex