Recovering the Binlog Position After an Unclean Shutdown
The pager fires: a primary hard-crashed and came back on its own. The database is up, replication is reconnecting, and the archiver is about to resume — but resuming blindly is how a crash turns into a corrupt recovery chain. If the archiver had captured the active tail just before the crash and the server then truncated a partial trailing transaction, the already-uploaded object is now longer than the recovered file, and a future replay will dead-end on the torn event. This page is the operator procedure for the minutes after an unclean shutdown: confirm crash recovery actually completed, read the authoritative recovered position, re-hash the last archived segment against the recovered file, and resume archiving only once the tail is proven consistent.
Context & Prerequisites
This is the runbook counterpart to the mechanism explained in Crash-Safe Binlog Recovery; read that for why the recovered tail — not the pre-crash bytes — is authoritative. You need MySQL 8.0.22+ with durable settings (sync_binlog=1, innodb_flush_log_at_trx_commit=1), Python 3.10+, shell access to the primary, and the archive manifest from Automated Binlog Archiving to Object Storage.
Step-by-Step Implementation
1. Confirm crash recovery completed before touching anything
Check the error log for the InnoDB crash-recovery banner and that the server is accepting connections; do not let the archiver resume while recovery is in progress.
# after an unclean shutdown
grep -E "Crash recovery|Recovering after a crash|ready for connections" \
/var/log/mysql/error.log | tail -5PITR relevance: reading a position mid-recovery captures a tail the server may still truncate; waiting for “ready for connections” guarantees the position is final.
2. Read the authoritative recovered position
Once up, the recovered tail is whatever the server now reports — this is the new source of truth.
-- MySQL 8.0.22+
SHOW BINARY LOG STATUS; -- File + Position of the recovered active tail
SELECT @@GLOBAL.gtid_executed; -- the recovered executed setPITR relevance: the recovered gtid_executed is what the archived chain must be contiguous with; any transaction the archive claims beyond this set is phantom and must be reconciled away.
3. Re-hash the last archived segment against the recovered file
If the newest closed segment on disk differs from what the manifest recorded, recovery truncated it — supersede the uploaded object.
# Python 3.10+
import hashlib
from pathlib import Path
def rehash(path: Path) -> str:
h = hashlib.sha256()
with path.open("rb") as fh:
while chunk := fh.read(1 << 20):
h.update(chunk)
return h.hexdigest()
def needs_resupersede(seg_name: str, binlog_dir: Path, manifest) -> bool:
local = binlog_dir / seg_name
return local.exists() and rehash(local) != manifest.sha256_of(seg_name)PITR relevance: a segment whose bytes changed after recovery was truncated; the pre-crash upload is invalid and must be replaced with the recovered version before it can serve a recovery.
4. Resume archiving from the reconciled tail
Only after the position is confirmed and the last segment re-verified, let the archiver re-enter its detect loop from the recovered state.
PITR relevance: resuming from the reconciled tail guarantees the next archived segment continues the chain contiguously, with no torn trailing transaction carried into object storage.
Configuration Snippet & Reference Table
| Step | Command / check | Why it matters for PITR |
|---|---|---|
| recovery done | error-log “ready for connections” | Position is final only after recovery completes. |
| recovered tail | SHOW BINARY LOG STATUS | The authoritative post-crash position. |
| executed set | SELECT @@GLOBAL.gtid_executed | What the archive must stay contiguous with. |
| re-hash | SHA-256 of newest closed segment | Detects a truncated last segment. |
| resume | archiver detect loop | Continues the chain from the reconciled tail. |
Verification Checklist
Gotchas & Version-Specific Caveats
Never RESET to tidy up. Running RESET BINARY LOGS AND GTID_SET after a crash discards the recovered tail and orphans the entire archived chain; let recovery restore the position.
SHOW MASTER STATUS is deprecated. Use SHOW BINARY LOG STATUS on 8.0.22+; the old form is removed as canonical in 8.4.
Relaxed durability widens the damage. With sync_binlog=0 a crash can lose durably-unlogged events and mark an Incident, which is a real chain hole — detect it with the gap check in Detecting GTID Gaps Before PITR Replay.
The active tail should never have been uploaded. If re-hashing finds a changed segment that was already confirmed, your archiver captured an active tail — fix the deferral rule per Building a Python Script to Sync Binlogs to S3 with Boto3.
Frequently Asked Questions
How do I know if recovery truncated my last segment?
Re-hash the newest closed segment on disk and compare it to the SHA-256 the manifest recorded at archive time. If they differ, recovery truncated a partial trailing transaction, so the file is now shorter than the uploaded object. Supersede the archived object with the recovered file. If the hashes match, the segment was already durable and nothing needs to change.
Do I need to intervene at all, or does MySQL handle it?
MySQL handles the database recovery automatically — it reconciles the binlog with InnoDB and restores a clean position. Your intervention is on the archiving side: making sure the archiver waits for recovery, trusts the recovered tail, and re-verifies the last segment. The server will not fix an already-uploaded truncated object for you; that reconciliation is yours.
Related
- Crash-Safe Binlog Recovery — the mechanism behind this procedure.
- Detecting GTID Gaps Before PITR Replay — catching a real chain hole after a lossy crash.
- Building a Python Script to Sync Binlogs to S3 with Boto3 — the active-tail deferral rule that prevents the problem.
Back to Crash-Safe Binlog Recovery.