carlos / platform Public

sim: hostlife: the admitted writer's start is finished by its own agent loop; a prepared landing generation keeps the flip's stamp; a compaction with a full input is full; the return's lagging marker

Found by the first rev 7.10 baselines: an admitted writer waiting for its
marker was resumed only by a wake (seed 5094 / 30,000), the probe's cold
admission into the landing's prepared generation issued a second stamp
and the probe was refused by its own record (seed 1117), a clean
rollback's source saw the marker lag behind the generation the return
named again (seed 9554), a prepared generation already opened was
re-entered (seeds 43 and 87 / 300), a target that never restored a
database returned a spurious dirty seal (seed 62 / 300), and a
compaction of a warm admission's full first upload into a delta lost a
deletion (seed 27371 / 30,000).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 5b7cf1de86ff1ab65aa66aaf78513b3f6e8e230e parent 7a7568c
4 files changed, +88 −10
  • internal/sim/hostlife/ltx_test.go +8 −0
  • internal/sim/hostlife/moves_test.go +5 −0
  • internal/sim/hostlife/protocol_test.go +44 −9
  • internal/sim/hostlife/sim_test.go +31 −1
diff --git a/internal/sim/hostlife/ltx_test.go b/internal/sim/hostlife/ltx_test.go
index 461a01d4b..766d2245a 100644
--- a/internal/sim/hostlife/ltx_test.go
+++ b/internal/sim/hostlife/ltx_test.go
@@ -513,6 +513,14 @@ func (w *world) compact(b *box) {
if max != 0 && !(v.B.Min <= max+1 && v.B.Max > max) {
return // non-contiguous inputs: litestream refuses to compact them
}
+ if v.B.Full {
+ // a full input anywhere makes the image complete from there: the
+ // output carries every page and a key the full file lacked is
+ // gone (rev 7.10: a warm admission's first upload is full, fact
+ // R5-1c, so full files appear mid-generation; seed 27371 /
+ // 30,000 compacted one into a delta and a deletion was lost)
+ out.Full = true
+ }
apply(out.Pages, v.B)
for k, val := range v.B.Pages {
if val == 0 {
diff --git a/internal/sim/hostlife/moves_test.go b/internal/sim/hostlife/moves_test.go
index 93ca71fb7..66d3029b2 100644
--- a/internal/sim/hostlife/moves_test.go
+++ b/internal/sim/hostlife/moves_test.go
@@ -172,6 +172,11 @@ func (w *world) role(b *box, c *claim) {
if c.Holder == nil {
return
}
+ if b.open != nil {
+ // an admitted writer finishing its start (rev 7.10 §7.14)
+ w.resumeStart(b, c)
+ return
+ }
switch c.Op.Kind {
case "moving":
if b.ID == c.Op.Src {
diff --git a/internal/sim/hostlife/protocol_test.go b/internal/sim/hostlife/protocol_test.go
index 6d18fb387..a1dc90e1c 100644
--- a/internal/sim/hostlife/protocol_test.go
+++ b/internal/sim/hostlife/protocol_test.go
@@ -437,12 +437,14 @@ func (w *world) spawn(b *box, c *claim, want string, probe bool) bool {
}
if bg != h.Gen && on("start-check") {
switch {
- case h.WriterBox == b.ID && w.ancestorGen(loc, h.Gen, bg):
- // the marker lags behind this box's own admission: handled below.
- // Another box's lagging marker is the start check's refusal
- // (the generation clause, J2-R2-8): a stale box whose region
- // holds none of holder.generation's files is not the admitted
- // writer that died between its CAS and its marker
+ case (h.WriterBox == b.ID || c.Op.Kind == "restoring" && b.ID == c.Op.Src) && w.ancestorGen(loc, h.Gen, bg):
+ // the marker lags behind this box's own admission, or behind
+ // the return that named the source's generation again:
+ // handled below. Another box's lagging marker is the start
+ // check's refusal (the generation clause, J2-R2-8): a stale
+ // box whose region holds none of holder.generation's files is
+ // not the admitted writer that died between its CAS and its
+ // marker
case w.ancestorGen(loc, bg, h.Gen):
// the marker names a descendant: a clean rollback returned the
// claim to the source's generation and abandoned the target's;
@@ -464,7 +466,10 @@ func (w *world) spawn(b *box, c *claim, want string, probe bool) bool {
source, _ = w.parentGen(loc, source)
}
w.stats["abandoned empty generation skipped"]++
- } else {
+ } else if !w.republishMarker(b, loc, h.Gen) {
+ // no earlier version names it: the admitted writer's marker
+ // never landed; from the operation's build journal for its
+ // prepared generation, else a fresh generation rooted at [1,1]
ver, key, tx := 0, "", 0
if h.Gen == installGen(&c.Op) && c.Op.BaseKey != "" {
ver, key, tx = c.Op.BaseVer, c.Op.BaseKey, c.Op.BaseTX
@@ -512,7 +517,7 @@ func (w *world) spawn(b *box, c *claim, want string, probe bool) bool {
return false
}
db.Stamp = b.stampOf(h.Inc)
- w.logf("box %d restored %s: TXID %d at position %d (%s admission into %q)", b.ID, dataKey(loc, source, ""), db.TX, db.Pos, adm.Mode, adm.NewGeneration)
+ w.logf("box %d restored %s: TXID %d at position %d, %d writes (%s admission into %q)", b.ID, dataKey(loc, source, ""), db.TX, db.Pos, len(db.Log), adm.Mode, adm.NewGeneration)
}
b.DB = db
if on("start-check") {
@@ -623,6 +628,25 @@ func (w *world) spawn(b *box, c *claim, want string, probe bool) bool {
return w.startProc(b, c, want, probe, adm.Mode)
}
+// resumeStart is the agent's own loop finishing an admitted writer's start
+// (rev 7.10 §7.14): the activator process holds the admission and waits
+// for the PUT it needs, without a wake. An admission the claim moved past
+// is abandoned.
+func (w *world) resumeStart(b *box, c *claim) {
+ o := b.open
+ if o == nil || c == nil || c.Holder == nil || hasMain(b) {
+ return
+ }
+ if o.Gen != c.Holder.Gen || o.Session != c.Holder.Session {
+ b.open = nil
+ w.stats["open: abandoned"]++
+ return
+ }
+ if w.resumeOpen(b, c, o) {
+ w.startProc(b, c, o.Want, false, o.Mode)
+ }
+}
+
// startProc starts the host's processes on b for the admitted writer.
func (w *world) startProc(b *box, c *claim, want string, probe bool, mode string) bool {
h := c.Holder
@@ -902,6 +926,7 @@ func (w *world) resumeOpen(b *box, c *claim, o *openState) bool {
}
if !visible() {
w.stats["open: waits for the marker"]++
+ w.logf("box %d: admitted into %q, waiting for its marker", b.ID, db.Gen)
return false
}
}
@@ -998,7 +1023,17 @@ func (w *world) admitCommit(b *box, c *claim, a admitInput) bool {
if a.Mode == "cold" {
w.newGen(n.Holder.Location, h.Gen, a.NewGeneration)
n.Holder.Gen = a.NewGeneration
- n.Holder.Stamp = w.issueStamp(n, b.ID) // the installing CAS (claim rev 6.8 §6.4a, §6.4)
+ if !(n.Op.Kind == "landing" && a.NewGeneration == installGen(&n.Op) && n.Holder.Stamp == n.Op.FlipGen) {
+ // a new installation: a new stamp (the installing CAS, claim
+ // rev 6.9 §6.4a, §6.4): a fresh generation, a push's prepared
+ // generation, a dirty rollback's return generation (claim rev
+ // 6.5: never the predecessor's stamp). The landing's prepared
+ // generation is the installation the flip CAS already made
+ // current with its stamp F, which the target's record
+ // activated before the probe; another stamp here would refuse
+ // the probe by its own record (seed 1117 / 30,000)
+ n.Holder.Stamp = w.issueStamp(n, b.ID)
+ }
e.Stamp = n.Holder.Stamp
if !a.PredecessorCertified && on("writes-lost") {
lost = &writesLost{Box: h.WriterBox, Gen: h.Gen, Session: h.Session, RemoteMaxTXID: a.RemoteMaxTXID}
diff --git a/internal/sim/hostlife/sim_test.go b/internal/sim/hostlife/sim_test.go
index 79e42919f..f88f63bbc 100644
--- a/internal/sim/hostlife/sim_test.go
+++ b/internal/sim/hostlife/sim_test.go
@@ -277,6 +277,9 @@ func (w *world) whyNotQuiescent() string {
if e := w.execBox(c); b != e && (b.Row != nil || len(b.Procs) > 0) {
out = append(out, fmt.Sprintf("box %d has a row or processes", b.ID))
}
+ if b.open != nil {
+ out = append(out, fmt.Sprintf("box %d admitted into %q, start in progress at stage %s", b.ID, b.open.Gen, b.open.stage))
+ }
}
}
if r := w.residue(); len(r) > 0 {
@@ -324,6 +327,9 @@ func (w *world) atRest() bool {
if b != e && (b.Row != nil || len(b.Procs) > 0) {
return false
}
+ if b.open != nil {
+ return false // an admitted writer still finishing its start
+ }
}
return true
}
@@ -577,8 +583,32 @@ func (w *world) checkDurable(c *claim, when string) {
// required write that is missing was caught above, and no key is ever
// masked; a later required write re-establishes its key's value
if why, ok := statesEqual(w.expectFrom(c.Holder.Inc, have), db.State); !ok {
- w.violate("INV-2 (cold restore, %s): the restored database differs from the replay of the writes it carries: %s (restored TXID %d; %s; %s)", when, why, db.TX, w.keyHistory(c.Holder.Inc, why, have), w.ltxShape(e.Region, dataKey(c.Holder.Location, c.Holder.Gen, "")))
+ w.violate("INV-2 (cold restore, %s): the restored database differs from the replay of the writes it carries: %s (restored TXID %d; %s; %s; %s)", when, why, db.TX, w.keyHistory(c.Holder.Inc, why, have), w.ltxShape(e.Region, dataKey(c.Holder.Location, c.Holder.Gen, "")), w.fileDetail(e.Region, dataKey(c.Holder.Location, c.Holder.Gen, ""), why))
+ }
+}
+
+// fileDetail lists, for diagnosis, each current file of the generation
+// with its kind, range, TXs and the page it holds for the key named in a
+// statesEqual message.
+func (w *world) fileDetail(r int, prefix, why string) string {
+ key := ""
+ if i := strings.Index(why, "key "); i >= 0 {
+ rest := why[i+4:]
+ if j := strings.Index(rest, " "); j > 0 {
+ key = rest[:j]
+ }
+ }
+ bk := w.buckets[r]
+ var out []string
+ for _, k := range bk.keys(prefix) {
+ v := bk.current(k)
+ if v == nil || v.B == nil || !isDataKey(k) {
+ continue
+ }
+ val, ok := v.B.Pages[key]
+ out = append(out, fmt.Sprintf("%s{full=%v txs=%v %s=%d/%v}", strings.TrimPrefix(k, prefix), v.B.Full, v.B.TXs, key, val, ok))
}
+ return "detail " + strings.Join(out, " ")
}
// keyHistory lists, for diagnosis, the acknowledged writes of inc that