From 943000ae8e50d14b5ffe7a321e744fa9f7aaf8aa Mon Sep 17 00:00:00 2001 From: pingqiu Date: Thu, 9 Apr 2026 15:46:00 -0700 Subject: [PATCH] fix: RebuildSourceDecision returns FullBase when CommittedLSN=0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- sw-block/engine/replication/history.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/sw-block/engine/replication/history.go b/sw-block/engine/replication/history.go index ce6986792..4a1f85d7d 100644 --- a/sw-block/engine/replication/history.go +++ b/sw-block/engine/replication/history.go @@ -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