carlos / platform Public

sim: hostlife: generation.json written If-Match the marker it supersedes (model 29)

Seeds 95625 and one other of 100,000: an earlier push's generation.json
PUT completed late over the later push's, so the claim named push10, the
bucket's marker said push7, and every recovery probe refused. The marker
is a reused key; its write is now conditional on the version it replaces.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 45e16b1d98f1e71ff9a9b756757d894581e17231 parent 93bf5ed
3 files changed, +31 −3
  • internal/sim/hostlife/moves_test.go +19 −3
  • internal/sim/hostlife/rules_test.go +1 −0
  • internal/sim/hostlife/world_test.go +11 −0
diff --git a/internal/sim/hostlife/moves_test.go b/internal/sim/hostlife/moves_test.go
index c127debfa..d74f75b44 100644
--- a/internal/sim/hostlife/moves_test.go
+++ b/internal/sim/hostlife/moves_test.go
@@ -732,7 +732,7 @@ func (w *world) landTarget(b *box, c *claim) {
w.cas(c, func(n *claim) bool { n.Op.LandPhase = "published"; return sameOp(n.Op.ID, op.ID) })
}
case "published":
- v := w.issue(b.ID, &request{kind: rPut, region: b.Region, key: genMarkerKey(loc), b: &blob{Kind: "gen", Gen: op.ID, Inc: c.Holder.Inc}})
+ v := w.publishGen(b, loc, op.ID, c.Holder.Inc)
if v > 0 {
w.cas(c, func(n *claim) bool { n.Op.LandPhase = "local"; return sameOp(n.Op.ID, op.ID) })
}
@@ -1043,7 +1043,7 @@ func (w *world) recoverSource(b *box, c *claim) {
w.cas(c, func(n *claim) bool { n.Op.SrcPhase = "published"; return sameOp(n.Op.ID, op.ID) })
}
case "published":
- v := w.issue(b.ID, &request{kind: rPut, region: b.Region, key: genMarkerKey(c.Holder.Location), b: &blob{Kind: "gen", Gen: op.RollbackID, Inc: c.Holder.Inc}})
+ v := w.publishGen(b, c.Holder.Location, op.RollbackID, c.Holder.Inc)
if v > 0 {
w.cas(c, func(n *claim) bool { n.Op.SrcPhase = "local"; return sameOp(n.Op.ID, op.ID) })
}
@@ -1136,7 +1136,7 @@ func (w *world) pushStep(b *box, c *claim) {
w.cas(c, func(n *claim) bool { n.Op.Phase = "published"; return sameOp(n.Op.ID, op.ID) })
}
case "published":
- v := w.issue(b.ID, &request{kind: rPut, region: b.Region, key: genMarkerKey(loc), b: &blob{Kind: "gen", Gen: op.ID, Inc: c.Holder.Inc}})
+ v := w.publishGen(b, loc, op.ID, c.Holder.Inc)
if v > 0 {
w.cas(c, func(n *claim) bool { n.Op.Phase = "restore"; return sameOp(n.Op.ID, op.ID) })
}
@@ -1277,3 +1277,19 @@ func (w *world) legacyRetired(c *claim) bool {
}
return false
}
+
+// publishGen writes generation.json, conditional on the marker it
+// supersedes: If-Match its version, or If-None-Match when there is none
+// (moves rev 7.1 §5.1, model 29). generation.json is a reused key, so an
+// earlier publication that completes late would otherwise put an older
+// generation back, and every wake would refuse.
+func (w *world) publishGen(b *box, loc, gen string, inc int) int {
+ r := &request{kind: rPut, region: b.Region, key: genMarkerKey(loc), b: &blob{Kind: "gen", Gen: gen, Inc: inc}}
+ if on("generation-conditional") {
+ r.ifSet = true
+ if cur := w.buckets[b.Region].current(r.key); cur != nil {
+ r.ifVer = cur.ID
+ }
+ }
+ return w.issue(b.ID, r)
+}
diff --git a/internal/sim/hostlife/rules_test.go b/internal/sim/hostlife/rules_test.go
index f1ebb2637..7c733e307 100644
--- a/internal/sim/hostlife/rules_test.go
+++ b/internal/sim/hostlife/rules_test.go
@@ -86,6 +86,7 @@ var allRules = []string{
"hold-op-open", // §5.2 conditional open
"start-check", // INV-4: markers agree
"start-check-claim-gen", // rev 6 §5.1 (R2-8): and name the generation the claim expects
+ "generation-conditional", // rev 7.1 §5.1 (model 29): generation.json is written If-Match the marker it supersedes
"markers-last", // R2-6
"revert-journal", // J-N11
"rollback-journal", // R2-3
diff --git a/internal/sim/hostlife/world_test.go b/internal/sim/hostlife/world_test.go
index 20db542ad..bedcb4fe1 100644
--- a/internal/sim/hostlife/world_test.go
+++ b/internal/sim/hostlife/world_test.go
@@ -198,6 +198,8 @@ type request struct {
srcVer int
doneAt int
beyond bool // completes after H
+ ifSet bool // a conditional PUT: If-Match ifVer, or If-None-Match when ifVer is 0
+ ifVer int
by string
// result, for an immediate completion
resultVer int
@@ -699,6 +701,15 @@ func (w *world) complete(r *request) int {
bk := w.buckets[r.region]
switch r.kind {
case rPut:
+ if r.ifSet {
+ cur := 0
+ if v := bk.current(r.key); v != nil {
+ cur = v.ID
+ }
+ if cur != r.ifVer {
+ return 0 // 412 Precondition Failed
+ }
+ }
v := &version{ID: w.newVer(), B: r.b, Straggler: r.beyond}
bk.objs[r.key] = append(bk.objs[r.key], v)
w.markDurable(r.key, r.b)