Put MySQL replicas in crash safe state before shutdown - #20599
Conversation
Signed-off-by: Matt Lord <mattalord@gmail.com>
Signed-off-by: Matt Lord <mattalord@gmail.com>
Review ChecklistHello reviewers! 👋 Please follow this checklist when reviewing this Pull Request. General
Tests
Documentation
New flags
If a workflow is added or modified:
Backward compatibility
|
Signed-off-by: Matt Lord <mattalord@gmail.com>
There was a problem hiding this comment.
Pull request overview
Adds crash-safe replica preparation before MySQL shutdown.
Changes:
- Makes relay logs durable and stops replication threads.
- Adds bounded shutdown preparation and error reporting.
- Adds unit and end-to-end coverage.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
go/vt/mysqlctl/replication.go |
Implements replica shutdown preparation. |
go/vt/mysqlctl/replication_test.go |
Tests preparation behavior and failures. |
go/vt/mysqlctl/mysqld.go |
Integrates preparation into shutdown. |
go/test/endtoend/mysqlctl/crashsafeshutdown/crash_safe_shutdown_test.go |
Verifies the shutdown durability fence. |
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #20599 +/- ##
===========================================
- Coverage 69.67% 68.35% -1.33%
===========================================
Files 1614 222 -1392
Lines 216793 32803 -183990
===========================================
- Hits 151044 22421 -128623
+ Misses 65749 10382 -55367
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Signed-off-by: Matt Lord <mattalord@gmail.com>
Signed-off-by: Matt Lord <mattalord@gmail.com>
|
Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits. |
timvaillancourt
left a comment
There was a problem hiding this comment.
Really nicely done — the defensiveness and test coverage here are excellent. Two best-effort edge cases I wanted to check on before approving:
-
The durability fence runs as one fail-fast batch (replication.go:280), so if
FLUSH ... BINARY LOGSfails (e.g. a replica with binlog off, or any transient flush error) it returns before theFLUSH ... RELAY LOGSand the receiver/applier stops — which is exactly the relay-durability + drain the PR is closing the gap on. Worth running the relay flush + thread stops independently of the binary-log flush (or ordering them first) so a benign earlier failure can't skip them? -
restoreReplicaAfterFailedShutdown(replication.go:431) gives up permanently if the initialGetDbaConnectionfails (or an in-loop status read errors), even though it has the generous 10-min budget. If mysqld is briefly unreachable right after a failed shutdown, the restore never runs and the replica is left with replication stopped. Intentional (treating connect-failure as "mysqld is exiting anyway"), or should the connect/status reads retry within the budget like START does?
Also tiny: the cmd != "" guard skips GR but not the filePos flavor's "unsupported", so file:pos replicas log two failed-stop warnings each shutdown.
Signed-off-by: Matt Lord <mattalord@gmail.com>
|
Thanks @timvaillancourt — all three were real issues, and they're addressed in 1fa9229:
|
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 14 out of 14 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (1)
go/vt/mysqlctl/mysqld.go:985
shutdownErrdoes not prove that shutdown was never initiated.executeShutdowncan return the caller deadline fromwaitForMysqldExitafter mysqladmin already deliveredSHUTDOWN(it explicitly recognizes this at lines 1087-1106). This branch then restores relaxed durability and restarts replication while mysqld may still be shutting down. If that hung shutdown is subsequently killed—the failure mode this fence is intended to protect—the fence may have been removed immediately before the crash. Preserve the fenced state when shutdown was accepted/in progress, and restore only when it is known that mysqld will remain running.
if replicaState != nil {
mysqld.armReplicaRestore(ctx, replicaState, nil)
timvaillancourt
left a comment
There was a problem hiding this comment.
Thanks — verified all three fixes from my last round and they hold up (the independent fence statements, the reconcile-based restore, the replicationThreadCommandAvailable guard). Another pass over the restore's failure paths found two more, both confirmed by running:
-
A takeover that can't reconnect drops restore ownership:
prepareReplicaForShutdowncancels the previous restoration and inherits its state, but a failed DBA connect returns nil before that state is retained; if the shutdown then also fails, no replacement restore is armed and the replica stays fenced (1/1/1, threads stopped) until someone notices. Observed with a scratch test:pending restores=0after the failed takeover -
The restore loop doesn't recognize
mysql.ErrNotReplica, so if the server stops being a replica mid-restore (promotion,RESET REPLICA ALL) it retriesSHOW REPLICA STATUSfor the full 10-minute budget andClosehangs with it — ~200 retries in 2s in a scratch test. The preparation handles this case atreplication.go:249; the restore needs the same check (anderrors.Israther than==while there)
Smaller: settingsRestored is set true even when the SET GLOBALs failed, so a transient error during the settings restore is never retried on the reconnected loop, unlike the thread restarts. Fail direction is safe (the fenced values are stricter), so maybe just a comment if the asymmetry is intentional
Signed-off-by: Matt Lord <mattalord@gmail.com>
Signed-off-by: Matt Lord <mattalord@gmail.com>
|
Thanks @timvaillancourt — all three were real, and they're addressed in ef05cbb:
Copilot then caught a follow-on in 3: with no threads to reconcile, a settings retry could spin on a broken connection (the status probe that resets one is never reached on that path) — fixed in f01f74c by reconnecting there too. |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 14 out of 14 changed files in this pull request and generated 1 comment.
Comments suppressed due to low confidence (1)
go/vt/mysqlctl/replication.go:607
- This completion condition ignores
settingsRestored. If a durabilitySETfails while the replication threads already report their desired state (for example, preparation timed out before reaching the stops), the function returns here and never performs the documented retry, leaving the live replica permanently on the fence's forced settings. RequiresettingsRestoredbefore declaring convergence.
if !needReceiver && !needApplier && !pendingReceiver && !pendingApplier && !cycleReceiver {
Signed-off-by: Matt Lord <mattalord@gmail.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 13 out of 14 changed files in this pull request and generated no new comments.
Suppressed comments (1)
go/vt/mysqlctl/replication.go:544
- These SETs run before checking whether the server is still a replica. If a failed-shutdown restoration races a promotion/
RESET REPLICA ALL, they can apply the replica's old relaxed values (for examplesync_binlog=0andinnodb_flush_log_at_trx_commit=2) to the new primary; the laterErrNotReplicabranch then returns with unsafe primary durability. Check replication status before restoring these values, and retain the full-durability fence when the server is no longer a replica.
for _, query := range []string{
"SET GLOBAL innodb_flush_log_at_trx_commit = " + state.flushLogAtTrxCommit,
"SET GLOBAL sync_binlog = " + state.syncBinlog,
"SET GLOBAL sync_relay_log = " + state.syncRelayLog,
} {
Signed-off-by: Matt Lord <mattalord@gmail.com>
Signed-off-by: Matt Lord <mattalord@gmail.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 15 out of 16 changed files in this pull request and generated no new comments.
Suppressed comments (1)
go/vt/mysqlctl/shutdown_flock.go:99
- The flock is retained until
Mysqld.Close, even after a shutdown attempt and any restoration have completed. This makes the lock effectively permanent for long-lived owners: vttablet keeps itsMysqlduntil process exit (go/cmd/vttablet/cli/cli.go:152-154), while backup shuts down and then restarts that same instance (go/vt/mysqlctl/builtinbackupengine.go:600,614). After the first such cycle, a separate mysqlctl/Mysqld process can no longer shut down the restarted server; it waits on this stale flock until its context expires. Retain the lock while an attempt or pending restore is active, but release it once that work reaches idle rather than only on object destruction.
mysqld.shutdownFlockMu.Lock()
mysqld.shutdownFlock = f
mysqld.shutdownFlockMu.Unlock()
Signed-off-by: Matt Lord <mattalord@gmail.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 15 out of 16 changed files in this pull request and generated 1 comment.
Suppressed comments (1)
go/vt/mysqlctl/mysqld.go:773
- Lock contention is not bounded by
preparationBudget; it waits only on the caller context. Some existing callers passcontext.Background()(for examplego/vt/mysqlctl/backup.go:481), while another process can retain this flock for the full 10-minute restore or longer while waiting for an unresolved preparation. Consequently this best-effort feature can delay the underlying shutdown far beyondshutdownTimeout, including past a pod termination grace period and into the SIGKILL failure mode this PR is intended to prevent. Bound cross-process waiting independently of caller context while preserving safe serialization or an explicit best-effort fallback.
preparationBudget := replicaShutdownPreparationBudget(shutdownTimeout)
if preparationBudget > 0 {
releaseFlock, err := mysqld.acquireShutdownFlock(ctx, cnf)
if err != nil {
return err
Description
When gracefully shutting down a
REPLICA/RDONLYMySQL instance, we now proactively place it in a crash-safe state first: full commit durability is restored (innodb_flush_log_at_trx_commit=1,sync_binlog=1,sync_relay_log=1), the already-written InnoDB redo, binary log, and relay log tails are flushed to be durable, and the replication threads are stopped so the multi-threaded applier drains to a clean, gap-free point.These settings are often relaxed on replicas — for example to speed up a catch-up after a restore — and if a shutdown is then interrupted (e.g. a SIGKILL after a hung shutdown, or a k8s volume detach) the replica can lose acknowledged writes or re-apply transactions on restart. We hit this in production as duplicate key errors that broke replication until manual repair.
The preparation is best effort and bounded, so it can never block or fail the shutdown itself. If the shutdown fails while mysqld keeps running, the previous replication and durability state is restored so the replica isn't left with replication stopped. Concurrent and retried shutdown attempts are serialized and honor caller cancellation.
Related Issue(s)
Fixes: #20688
Fixes: #19625
Checklist
Deployment Notes
AI Disclosure
I worked with Claude and Fable 5 on the work and Codex and GPT-5.6 on the reviews.