Refuse XA workloads in the binlog stream instead of applying prepared rows - #1079
Draft
morgo wants to merge 3 commits into
Draft
Refuse XA workloads in the binlog stream instead of applying prepared rows#1079morgo wants to merge 3 commits into
morgo wants to merge 3 commits into
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>
Contributor
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.
morgo
marked this pull request as draft
July 28, 2026 12:23
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Both change clients treated an XA transaction's prepare-time row events as committed. MySQL writes an XA transaction's entire first binlog group at
XA PREPAREtime —GTIDEvent → Query("XA START") → row events → Query("XA END") → XA_PREPARE_LOG_EVENT— before the transaction's outcome is known. Spirit buffered those row events like any committed transaction and flushed them to the target (the GTID client even promoted the prepare-group GTID into its resume set at theXA_PREPARE_LOG_EVENT).If the transaction was later terminated with
XA ROLLBACK, nothing in the binlog undoes those rows: the target diverges permanently, detectable only by checksum — and only if the affected chunk is checksummed after the rollback. The pre-existing test suite explicitly documented the hazard ("a later XA ROLLBACK of the prepared transaction would not be compensated").Both replication paths were exposed: the GTID client had explicit XA handling that applied prepared rows, and the file/offset client had no XA handling at all — XA statements fell through to the DDL parser (unparseable → skipped) while the prepare-group's row events were buffered and applied identically.
Fix: fail fast, not full XA support
Full XA support (tracking prepared XIDs, buffering until
XA COMMIT/XA ROLLBACK, undoing on rollback) is out of scope. Instead, spirit now refuses XA workloads outright — the same posture as the preflight refusal of non-emptybinlog_row_value_options:Any XA statement observed as a QueryEvent (
XA START,XA END,XA COMMIT,XA ROLLBACK) and anyXA_PREPARE_LOG_EVENTfails the stream in both clients, for uncompressed streams andbinlog_transaction_compressionpayloads alike. The abort propagates through the existing fatal stream-error path (FatalReasonStreamError— the same one the minimal-RBR runtime guard uses), so the migration stops cleanly and the checkpoint is preserved.Why the abort provably happens before divergence
Rows reach the target only via subscription buffers that a flush later applies. The guard fires at the group's opening
"XA START"QueryEvent, which the server writes ahead of the transaction's row events (verified against MySQL 8.0, uncompressed and compressed): the stream is torn down before any of those row events are decoded or buffered, so no flush — concurrent or later — can ever apply them. There is no race window to lose. The prepare-group GTID (and file/offset position) is deliberately not advanced past the refused group, so a checkpoint resume replays the group and re-refuses it rather than skipping its rows. TheXA_PREPARE_LOG_EVENTbranches are defense in depth in case a future server version reshapes the group.Terminal
XA COMMIT/XA ROLLBACKstatements with no preceding in-streamXA START(a transaction prepared before spirit connected) are refused too: their row events were never streamed and may postdate the copier's snapshot of their chunk, so an applied commit could silently lose rows.Behavior notes
XA COMMIT ... ONE PHASE(atomic, no prepared state) previously replicated correctly and is now also refused — deliberately, because at"XA START"time the one-phase outcome is unknowable, and only refusing there guarantees no prepared rows can ever be flushed.binlog_row_image=FULLruntime guard); once the workload stops using XA, a fresh start proceeds normally.Why runtime-only (no preflight)
There is no reliable server-level "workload uses XA" signal to check up front: XA cannot be disabled server-side, and
XA RECOVERrequires theXA_RECOVER_ADMINprivilege while only showing transactions prepared at that instant — an empty result says nothing about what the workload will do mid-migration. The runtime guard is deterministic and fires before any divergence is possible, so a preflight would add a privilege requirement without adding safety.Tests
binlog_transaction_compression): the guard fires as a checkpoint-preserving stream error at prepare time, zero row events buffered, and the target contains nothing afterXA ROLLBACK— for both the GTID and file/offset clients.XA START, lone terminalXA COMMIT/XA ROLLBACK, loneXA_PREPARE_LOG_EVENT), asserting the fatal reason, empty buffers, and that the XA GTID never enters the resume set.xa).🤖 Generated with Claude Code