sim: hostlife: a clean close is refused while an escapee runs or when its certificate PUT is not acknowledged; a resumed step carries the stamp active now; strict-spawn caught alone
Found by the second rev 7.10 100,000-seed baseline: a close waiting for an escaped descendant, and one waiting for a certificate PUT late by three horizons, held a revert's probe until it stuck (seed 97950); a stalled probe step carried the stamp active at its read, before a cold admission's installing CAS, and the box's record refused its own probe (seed 67671). The strict-spawn mutation is caught alone by the sweep now (a descendant surviving the idle stop acknowledges writes the purge then loses), so its expected-miss line goes.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
3 files changed,
+37
−6
internal/sim/hostlife/expected-outcomes.txt+0 −1internal/sim/hostlife/ltx_test.go+29 −4internal/sim/hostlife/moves_test.go+8 −1
diff --git a/internal/sim/hostlife/expected-outcomes.txt b/internal/sim/hostlife/expected-outcomes.txt| index cfb23c5e5..5bbbdea22 100644 |
| --- a/internal/sim/hostlife/expected-outcomes.txt |
| +++ b/internal/sim/hostlife/expected-outcomes.txt |
| @@ -12,7 +12,6 @@ exec-admission missed one of four admission fences (claim rev 6.8.1 adds exec-wr |
| hold-op-open missed one of four admission fences; caught with exec-admission, start-check-claim-gen and exec-writer |
| start-check-claim-gen missed one of four admission fences; caught with exec-admission, hold-op-open and exec-writer |
| start-check missed subsumed since rev 7.10: a box restores only its own region's remote of holder.generation, so with the whole start check gone the stale source of a cross-region move finds nothing to restore and cannot start beside the target; the witness is retired, the generation clause (start-check-claim-gen) keeps its witness |
| -strict-spawn missed caught with evidence-whole-tree |
| touched-incomplete missed caught with legacy-discovery |
| door-reread missed subsumed by C3a and, since rev 7.8, by writer epochs (claim rev 6.6): a recreated incarnation writes into a fresh generation the listing never selects; its witness is retired |
| door-by-version missed subsumed by C3a and, since rev 7.8, by writer epochs (claim rev 6.6): a recreated incarnation writes into a fresh generation the listing never selects; its witness is retired |
diff --git a/internal/sim/hostlife/ltx_test.go b/internal/sim/hostlife/ltx_test.go| index 766d2245a..0c486368e 100644 |
| --- a/internal/sim/hostlife/ltx_test.go |
| +++ b/internal/sim/hostlife/ltx_test.go |
| @@ -362,11 +362,21 @@ func (db *localDB) settled(now int) bool { |
| // stops. |
| func (w *world) closeStep(b *box) { |
| db := b.DB |
| - if db == nil || !db.Closing || len(b.Procs) > 0 { |
| + if db == nil || !db.Closing || hasMain(b) { |
| + return |
| + } |
| + if len(b.Procs) > 0 { |
| // nothing of the host may run when the session certifies: a |
| - // descendant that outlived the main process can still acknowledge |
| - // writes the certificate would not cover, which the next admission's |
| - // purge would lose silently (rev 7.10; seed 17 / 300) |
| + // descendant that outlived the unit stop (an escapee outside the |
| + // cgroup) can still acknowledge writes the certificate would not |
| + // cover, which the next admission's purge would lose silently (rev |
| + // 7.10; seed 17 / 300). The close is refused, not delayed: the |
| + // session stays uncertified, the next wake is cold and declares |
| + // (seed 97950 / 100,000: a close that waited for an escapee held |
| + // the revert's probe until it stuck) |
| + db.Closing, db.CloseReq = false, nil |
| + w.stats["clean close refused: a process still runs"]++ |
| + w.logf("box %d: clean close refused, a descendant of the host still runs", b.ID) |
| return |
| } |
| // the certificate is the live generation's: once an operation has |
| @@ -396,7 +406,11 @@ func (w *world) closeStep(b *box) { |
| // is unknown for good, so this session never certifies |
| db.Closing = false |
| w.stats["clean close refused: uncertain"]++ |
| + w.logf("box %d: clean close refused, the session is uncertain", b.ID) |
| + return |
| } |
| + inflight := db.InFlight != nil && !db.InFlight.done |
| + w.logf("box %d: clean close waits (queue %d, upload in flight %v, %d requests tracked)", b.ID, len(db.Queue), inflight, len(db.Issued)) |
| return |
| } |
| if db.CloseReq == nil { |
| @@ -407,6 +421,17 @@ func (w *world) closeStep(b *box) { |
| } |
| if db.CloseReq.done { |
| db.Closing, db.CloseReq = false, nil |
| + } else if w.now-db.CloseReq.at >= uploadTimeout { |
| + // the certificate PUT itself was not acknowledged within the |
| + // client's wait: the close ends without a certificate the box can |
| + // rely on; the PUT may still land, and a later wake that finds it |
| + // may continue warm, since its content is right (the drain was |
| + // complete); meanwhile the next wake is cold (seed 97950 / 100,000: |
| + // a certificate PUT late by three horizons held the revert's probe |
| + // until it stuck) |
| + db.Closing, db.CloseReq = false, nil |
| + w.stats["clean close: certificate not acknowledged"]++ |
| + w.logf("box %d: the certificate PUT was not acknowledged; the close ends uncertified", b.ID) |
| } |
| } |
diff --git a/internal/sim/hostlife/moves_test.go b/internal/sim/hostlife/moves_test.go| index 66d3029b2..5cc2902b4 100644 |
| --- a/internal/sim/hostlife/moves_test.go |
| +++ b/internal/sim/hostlife/moves_test.go |
| @@ -135,7 +135,14 @@ func (w *world) resumeStale(b *box, old *claim) { |
| if on("revalidate-before-effect") && !sameStep(old, c) { |
| return |
| } |
| - w.role(b, old.clone()) |
| + // the revalidation re-reads the box's own record as well: the stamp the |
| + // resumed step carries is the one active now (claim rev 6.5 §6.4; seed |
| + // 67671 / 100,000: a stalled probe step carried the stamp active at its |
| + // read, before a cold admission's installing CAS, and the box's record |
| + // refused its own probe) |
| + old = old.clone() |
| + old.readStamp = c.readStamp |
| + w.role(b, old) |
| } |
| // role is the worker's exec-lease renewal, quarantine and operation role, |