fix: RebuildSourceDecision returns FullBase when CommittedLSN=0

When CommittedLSN=0 (sync_all mode, replica degraded), snapshot-tail
rebuild was chosen because IsRecoverable(checkpoint, 0) is vacuously
true (0 <= HeadLSN always). But snapshot-tail requires a valid committed
endpoint for tail-replay. Without it, ExecuteRebuildPlan calls
TransferSnapshot which RemoteRebuildIO doesn't support → immediate fail.

Fix: if CommittedLSN=0, force RebuildFullBase. This is the correct
source when the primary has data but no replica has confirmed durability.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
pingqiu
2026-04-09 15:46:00 -07:00
co-authored by Claude Opus 4.6
parent a79cba0be7
commit 943000ae8e
+6
View File
@@ -65,6 +65,12 @@ func (rh *RetainedHistory) IsRecoverable(startExclusive, endInclusive uint64) bo
// 2. The WAL tail from CheckpointLSN to CommittedLSN is replayable
// (i.e., CheckpointLSN >= TailLSN and CommittedLSN <= HeadLSN)
func (rh *RetainedHistory) RebuildSourceDecision() (source RebuildSource, snapshotLSN uint64) {
// CommittedLSN=0 means no lineage-safe boundary exists (e.g., replica is
// degraded in sync_all mode). Snapshot-tail requires a valid committed
// endpoint for the tail-replay range. Without it, fall back to full-base.
if rh.CommittedLSN == 0 {
return RebuildFullBase, 0
}
if rh.CheckpointTrusted && rh.CheckpointLSN > 0 &&
rh.IsRecoverable(rh.CheckpointLSN, rh.CommittedLSN) {
return RebuildSnapshotTail, rh.CheckpointLSN