GTID master failover recovery + integration test (GTID failover 3/3) - #453
Open
driv3r wants to merge 2 commits into
Open
GTID master failover recovery + integration test (GTID failover 3/3)#453driv3r wants to merge 2 commits into
driv3r wants to merge 2 commits into
Conversation
8 tasks
driv3r
force-pushed
the
gtid-stage8-gtid-failover
branch
from
July 28, 2026 08:14
49ed25c to
9c3397d
Compare
When the source master fails over, the binlog streamer previously died: a
non-timeout GetEvent error went straight to ErrorHandler.Fatal, and
go-mysql's syncer only reconnects to the same host. This adds opt-in,
GTID-only recovery that reconnects the whole run to the promoted writer.
Built on SourceRuntime: failover is an in-process resume onto a new source
session rather than a hand-repoint of each consumer.
- SourceReconnector is the seam between the streamer ("I lost the source;
here's the GTID set I must not fall behind") and the Ferry, which owns
the SourceRuntime. ferrySourceReconnector resolves the new writer, then
SourceRuntime.ReplaceValidated opens one connection, validates it, and
only publishes it to consumers if valid. Because every source consumer
already reads from the SourceRuntime, that single swap repoints the whole
run.
- validateFailoverTarget fails closed: the candidate must be a writer (not
read_only), have gtid_mode=ON, and its executed set must contain
everything already applied downstream -- the committed streamed set plus
any in-flight transaction's GTID (tracked from GTIDEvent/XIDEvent). If a
cutover stop target is recorded, the candidate must contain it too, or
streaming would hang. An undecodable in-flight GTID aborts recovery
rather than validating against an under-approximated set.
- On success the streamer rebuilds its syncer against the promoted writer
and restarts StartSyncGTID from the last resumable set (replaying the
interrupted transaction). DB/DBConfig swaps are under connMu; FlushAndStop
reads currentDB() so cutover records the stop coordinate from the new
writer. On recovery exhaustion the Run loop fatals and returns.
- Bounds go-mysql's same-host reconnect retries (MaxReconnectAttempts,
default 3) when failover is enabled. Without this the syncer retries the
dead host forever and GetEvent only ever returns context timeouts, so
recovery would never trigger. Behavior is unchanged when failover is
disabled. Tunable via SyncerMaxReconnectAttempts.
Config.MasterFailoverRecovery is validated (requires gtid mode + resolver)
and wired onto the source streamer only; the target verifier streams from
the stable target and does not fail over.
Drives a full source master failover and asserts Ghostferry reconnects to
the promoted writer and finishes the move against it.
The test controls ordering directly (no status-callback timing races):
- sets mysql-3 (port 29293, already in the compose files) up as a GTID
replica of the source, aligning gtid_purged so it holds the real seed
and later writes;
- runs the ferry, and only AFTER row copy completes (so the data iterator
is done reading the source) writes extra rows, waits for mysql-3 to
replicate them, promotes mysql-3, and stops the source container;
- synchronizes on SourceRuntime().Config().Port flipping to the promoted
port before driving cutover, so cutover records its stop coordinate
from the new writer;
- asserts the promoted master and target are identical and that the ferry
did not fatal.
GTID-only (skips otherwise) and requires docker/podman to stop the source
container (skips otherwise). start-mysql.sh now also starts/waits for
mysql-3.
driv3r
force-pushed
the
gtid-stage8-gtid-failover
branch
from
July 28, 2026 15:16
9c3397d to
c94372d
Compare
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.
What
Implements opt-in, GTID-only master failover recovery: when the source MySQL master fails over, Ghostferry reconnects the whole run to the promoted writer instead of dying. Includes a deterministic Go integration test that proves it end-to-end.
PR 3 of 3 of the GTID failover rework:
SourceRuntime(Introduce SourceRuntime to own the source connection (GTID failover 1/3) #451).SourceRuntime(Migrate source consumers to SourceRuntime (GTID failover 2/3) #452).How
Two commits.
Add GTID master failover recoveryFailover is an in-process resume onto a new source session, not a hand-repoint of each consumer:
SourceReconnectoris the seam between the streamer ("I lost the source; here's the GTID set I must not fall behind") and the Ferry, which owns theSourceRuntime.ferrySourceReconnectorresolves the new writer, thenSourceRuntime.ReplaceValidatedopens one connection, validates it, and only publishes it if valid. Because every source consumer already reads from theSourceRuntime(Migrate source consumers to SourceRuntime (GTID failover 2/3) #452), that single swap repoints the entire run.validateFailoverTarget): candidate must be a writer (not@@read_only), have@@GLOBAL.gtid_mode = ON, and its executed set must contain everything already applied downstream — the committed streamed set plus any in-flight transaction's GTID (tracked fromGTIDEvent/XIDEvent). If a cutover stop target is recorded, the candidate must contain it too. An undecodable in-flight GTID aborts recovery. A candidate that fails validation is never published.StartSyncGTIDfrom the last resumable set. DB/DBConfig swaps are underconnMu;FlushAndStopreadscurrentDB()so cutover records the stop coordinate from the new writer. On recovery exhaustion the Run loop fatals and returns.MaxReconnectAttempts, default 3) when failover is enabled — otherwise the syncer retries the dead host forever andGetEventonly ever returns timeouts, so recovery would never trigger. Unchanged when failover is disabled; tunable viaSyncerMaxReconnectAttempts.Config.MasterFailoverRecoveryis validated (requires gtid mode + resolver) and wired onto the source streamer only.Add Go integration test for master failover recoveryA deterministic Go test (
test/go) that drives the ferry directly, avoiding the status-callback timing races of a Ruby harness test:mysql-3(port 29293, already in the compose files) up as a GTID replica of the source (aligninggtid_purgedso it holds the real data);SourceRuntime().Config().Portflipping to the promoted port before cutover;GTID-only (skips otherwise); requires
docker/podmanto stop the source container (skips otherwise).start-mysql.shnow also starts/waits for mysql-3.Testing
ReplaceValidatedpublish/reject, reconnector resolver-error/nil-candidate paths, in-flight GTID tracking, config validation.-raceclean.test/gosuite green in bothfile_positionandgtidmodes.go build+gofmtclean.