sim: hostlife: the recovery build takes stop evidence first and reuses a current snapshot on replay
Seed 86799 of 100,000: a descendant that outlived the main process wrote to the warm database between the recovery's snapshot and the generation moving, so the write was acknowledged, uploaded into the old generation and absent from the new one. The build now requires stop evidence before it takes its baseline, as every other operation does. A replayed build that finds a snapshot current in the new generation reuses it as the baseline and reuses a current anchor, never rebuilding from another baseline under the same key.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
1 file changed,
+26
−5
internal/sim/hostlife/moves_test.go+26 −5
diff --git a/internal/sim/hostlife/moves_test.go b/internal/sim/hostlife/moves_test.go| index 6782bf8d7..ab2339697 100644 |
| --- a/internal/sim/hostlife/moves_test.go |
| +++ b/internal/sim/hostlife/moves_test.go |
| @@ -1625,6 +1625,9 @@ func (w *world) anchorAcked(b *box, loc, gen, snapKey string, snapVer, tx int) b |
| if sv == nil { |
| return false |
| } |
| + if w.buckets[b.Region].current(ltxKey(loc, gen, 0, tx, tx)) != nil { |
| + return true // a replay finds its anchor current and reuses it |
| + } |
| db := &localDB{Inc: sv.B.Inc, Loc: loc, Gen: gen, Key: sv.B.SealKey, Pos: tx, State: cloneState(sv.B.Pages), Log: append([]int(nil), sv.B.TXs...)} |
| acked := w.ackedPut(b, ltxKey(loc, gen, 0, tx, tx), anchorOf(db, sv.B.Inc), false) |
| w.stats["restore anchors"]++ |
| @@ -1655,11 +1658,29 @@ func (w *world) recoverStep(b *box, c *claim) { |
| loc := h.Location |
| switch op.Phase { |
| case "build": |
| + // nothing of the host may run while the baseline is taken: a |
| + // descendant that outlived the main process would write to the |
| + // database after the snapshot and before the generation moved |
| + // (seed 86799: a write acknowledged during the build was lost) |
| + if !w.evidence(b, op.ID) { |
| + return |
| + } |
| var base *localDB |
| - if b.DB != nil && b.DB.Inc == h.Inc && b.DB.Gen == op.BaseGen { |
| + reused := false |
| + bk := w.buckets[b.Region] |
| + if snaps := w.currentLTX(bk, dataKey(loc, op.ID, ""), 9); len(snaps) > 0 { |
| + // a replayed build (a crash after the snapshot landed, with or |
| + // without the disk): the snapshot already current in the new |
| + // generation is reused, never rebuilt from another baseline, |
| + // which could carry other bytes under the same key (R7-4, R6-5; |
| + // seed 86799: the first build's snapshot held a write the |
| + // rebuilt one from the old generation did not) |
| + sv := snaps[len(snaps)-1].B |
| + base = &localDB{Inc: h.Inc, Loc: loc, Gen: op.ID, Key: sv.SealKey, Pos: sv.Max, State: cloneState(sv.Pages), Log: txsOf(sv), TX: w.lineageTX(sv)} |
| + reused = true |
| + w.stats["cold recovery: snapshot reused on replay"]++ |
| + } else if b.DB != nil && b.DB.Inc == h.Inc && b.DB.Gen == op.BaseGen { |
| base = b.DB |
| - } else if b.DB != nil && b.DB.Inc == h.Inc && b.DB.Gen == op.ID { |
| - base = b.DB // a replayed build after a crash: the restored file is there |
| } else { |
| base = w.restoreCalc(b.Region, h.Inc, loc, op.BaseGen) |
| if base == nil { |
| @@ -1674,10 +1695,10 @@ func (w *world) recoverStep(b *box, c *claim) { |
| } |
| n := base.Pos |
| key := snapKey(loc, op.ID, n) |
| - if !w.ackedPut(b, key, snap, false) { |
| + if !reused && !w.ackedPut(b, key, snap, false) { |
| return |
| } |
| - sv := w.buckets[b.Region].current(key) |
| + sv := bk.current(key) |
| if sv == nil { |
| return |
| } |