carlos / platform Public

hostlife model: rev 7.17, joint round 16 and claim rev 6.24

R16-2: a park executes strictly within its scope's namespace. The sweep
groups its residue by scope location (an incarnation's own, the legacy
instances/<host> which excludes child incarnations, moves/<host>) and
queues one chore per location with that scope and its archive
incarnation; the archive destination names the incarnation; the
runner confines every scoped chore's listing to its scope (a staging
lane to its own lane, an instances scope never to moves/) and INV-23
checks every key a chore would archive for membership. The delete's park
is scoped to the deleted incarnation's own location and the staging
namespace, never the whole host prefix. Rule scope-membership (the
mutation is rev 7.16's whole-prefix listing); witness
wSweepScopeMembership.

R16-3: the legacy identity is session 0 with generation "" (@root):
adoption, the pre-release processes and their acknowledgements, and the
witness fixtures carry it; INV-24 (every declaration names a box;
session 0 only in the origin generation); a created incarnation's first
admission declares nothing, its predecessor has no writer.
TestLegacyLostTargetReturn: adopt, a move with src_covered false, a
cold target admission, the target declared lost, the return declares
the target's session and the source's cut session 0.

R16-1: TestStagingSurvivesLegacyPark, sealed staging beside a legacy
instance park. Born-covered narrowed to a park scoped exactly to
holder.location or an unscoped park-replica (claim rev 6.24 item 5).
Rules: 107. Witnesses: 73.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev b9e97783296547e9b4770a81d50b4616d98611e6 parent 6f2a93c
7 files changed, +326 −33
  • internal/sim/hostlife/background_test.go +9 −5
  • internal/sim/hostlife/console_test.go +10 −1
  • internal/sim/hostlife/moves_test.go +21 −11
  • internal/sim/hostlife/protocol_test.go +96 −6
  • internal/sim/hostlife/rules_test.go +1 −0
  • internal/sim/hostlife/sim_test.go +3 −3
  • internal/sim/hostlife/witness_test.go +186 −7
diff --git a/internal/sim/hostlife/background_test.go b/internal/sim/hostlife/background_test.go
index bb5781ef5..642f71695 100644
--- a/internal/sim/hostlife/background_test.go
+++ b/internal/sim/hostlife/background_test.go
@@ -167,11 +167,15 @@ func (w *world) adopt() bool {
if eb == nil {
return false
}
- // adoption writes holder.stamp (claim rev 6.5 §2); the session starts
- // with it, and the pre-release process on the executing box is its
- // writer (rev 7.11: the first cold admission's writes_lost names it;
- // seed 94288 / 100,000 named box 0 and left the legacy writes undeclared)
- n := &claim{Ver: 1, Holder: &holder{Inc: w.rec.Inc, App: w.rec.App, Legacy: true, Location: locationFor(w.rec.Inc, true), Stamp: 1, Session: 1, WriterBox: eb.ID},
+ // adoption writes holder.stamp (claim rev 6.5 §2). The pre-claim writer
+ // is the legacy identity (claim rev 6.24, R16-3): session 0 with
+ // generation "" (read as @root), the pre-release process on the
+ // executing box its writer_box; the first cold admission's writes_lost
+ // names it (seed 94288 / 100,000 named box 0 and left the legacy writes
+ // undeclared), and a lost-target return declares it in
+ // additional_writes_lost. Rev 7.13 to 7.16 gave it session 1, which hid
+ // the session-zero case from E
+ n := &claim{Ver: 1, Holder: &holder{Inc: w.rec.Inc, App: w.rec.App, Legacy: true, Location: locationFor(w.rec.Inc, true), Stamp: 1, Session: 0, WriterBox: eb.ID},
Op: opState{Kind: "serving", Exec: eb.ID, Parts: []int{eb.ID}}, Regions: []int{w.rec.Region}, Boxes: []int{eb.ID}, Complete: false}
if w.stampBox == nil {
w.stampBox = map[int]int{}
diff --git a/internal/sim/hostlife/console_test.go b/internal/sim/hostlife/console_test.go
index 48aeea4bc..89c0e50d7 100644
--- a/internal/sim/hostlife/console_test.go
+++ b/internal/sim/hostlife/console_test.go
@@ -476,8 +476,17 @@ func (w *world) parkingObligations(n *claim, inc int) {
boxes = addUniq(boxes, b.ID)
}
}
+ // the deleted incarnation's own location (the legacy instances/<host>
+ // for a legacy host, which excludes child incarnations; claim rev
+ // 6.24 §2) and the staging namespace, each its own chore; an older
+ // incarnation's residue is the at-rest sweep's (R3-4). The mutation of
+ // scope-membership parks the whole host prefix, as rev 7.16 did
+ loc := "instances/" + host + "/"
+ if n.Holder != nil && on("scope-membership") {
+ loc = n.Holder.Location
+ }
for _, r := range regions {
- for _, p := range []string{"instances/" + host + "/", "moves/" + host + "/"} {
+ for _, p := range []string{loc, "moves/" + host + "/"} {
n.Obl = append(n.Obl, w.newObl(n, "park-replica", r, -1, p, fmt.Sprint(inc), 0))
}
}
diff --git a/internal/sim/hostlife/moves_test.go b/internal/sim/hostlife/moves_test.go
index c21e361ba..ed2a73a8e 100644
--- a/internal/sim/hostlife/moves_test.go
+++ b/internal/sim/hostlife/moves_test.go
@@ -1666,12 +1666,26 @@ func (w *world) sweep(b *box, c *claim) {
}
}
bk := w.buckets[b.Region]
+ // one chore per scope location holding residue (claim rev 6.24 §2,
+ // R16-2): the sweepable keys are grouped by the location they belong
+ // to, an incarnation's own, the legacy instances/<host> (a level
+ // directory or generation directly under the host prefix, never a
+ // child incarnation) or moves/<host>; each chore's scope names that
+ // location and its archive incarnation. The mutation of
+ // scope-membership queues rev 7.16's two chores, instances/<host> and
+ // moves/<host>, over the whole prefixes
var prefixes []string
for _, p := range []string{"instances/" + host + "/", "moves/" + host + "/"} {
for _, k := range bk.keys(p) {
- if hasNonMarker(bk.objs[k]) && w.sweepable(c, b.Region, k) {
- prefixes = append(prefixes, p)
- break
+ if !hasNonMarker(bk.objs[k]) || !w.sweepable(c, b.Region, k) {
+ continue
+ }
+ loc := p
+ if on("scope-membership") {
+ loc = scopeOf(k) + "/"
+ }
+ if !hasStr(prefixes, loc) {
+ prefixes = append(prefixes, loc)
}
}
}
@@ -1704,17 +1718,13 @@ func (w *world) sweep(b *box, c *claim) {
op = n.Op.ID
w.stats["holderless sweeps"]++
}
- inc := 0
- if n.Holder != nil {
- inc = n.Holder.Inc
- } else if len(n.Retired) > 0 {
- inc = n.Retired[len(n.Retired)-1] // a free host's sweep archives under the newest retired incarnation (claim rev 6.23 §2)
- }
for _, p := range prefixes {
// the scope is persisted at the queuing CAS (claim rev 6.23 §2):
// the sweeping box's complete listing is the evidence, taken now;
- // the location instances/<host> or moves/<host>
- n.Obl = append(n.Obl, w.scoped(obligation{ID: w.newID("ob"), Op: op, Kind: "park-sweep", Region: b.Region, Box: -1, Prefix: p, Ret: "sweep"}, w.now, inc))
+ // the location and its archive incarnation (the location's own,
+ // the holder's for the legacy location or the staging sweep, the
+ // newest retired on a free host; claim rev 6.24 §2)
+ n.Obl = append(n.Obl, w.scoped(obligation{ID: w.newID("ob"), Op: op, Kind: "park-sweep", Region: b.Region, Box: -1, Prefix: p, Ret: "sweep"}, w.now, incOf(n, scopeLocation(p))))
}
// CoverParkedPredecessors at the queuing CAS (claim rev 6.21 item 6):
// the predecessors with a sweepable object, no others
diff --git a/internal/sim/hostlife/protocol_test.go b/internal/sim/hostlife/protocol_test.go
index f811d108b..b7986dd7d 100644
--- a/internal/sim/hostlife/protocol_test.go
+++ b/internal/sim/hostlife/protocol_test.go
@@ -1781,7 +1781,12 @@ func (w *world) admitCommit(b *box, c *claim, a admitInput) bool {
// after the release queued the target region's park, and
// CoverRegionPredecessors in that CAS could not see this record
for _, o := range n.Obl {
- if o.Kind == "park-superseded" && o.Region == e.PredRegion && o.Prefix == n.Holder.Location {
+ // narrowed (claim rev 6.24 item 5): a park scoped exactly to
+ // holder.location, or an unscoped park-replica; a scoped park of
+ // an old incarnation or of a staging prefix does not count
+ exact := o.Kind == "park-superseded" && o.Prefix == n.Holder.Location && (o.Scope == nil || o.Scope.Location == scopeLocation(n.Holder.Location))
+ unscopedReplica := o.Kind == "park-replica" && o.Scope == nil
+ if (exact || unscopedReplica) && o.Region == e.PredRegion {
e.Covered = true
w.stats["admissions born covered: whole-location park pending"]++
break
@@ -1802,12 +1807,17 @@ func (w *world) admitCommit(b *box, c *claim, a admitInput) bool {
n.Holder.Stamp = w.issueStamp(n, b.ID)
}
e.Stamp = n.Holder.Stamp
- if a.Coverage == "" && !a.PredecessorCertified && on("writes-lost") {
+ if a.Coverage == "" && !a.PredecessorCertified && p.Box >= 0 && on("writes-lost") {
// only an uncovered, uncertified predecessor is declared
// (claim rev 6.12): a sealed or clean-return coverage holds
- // every write it acknowledged
+ // every write it acknowledged. A created incarnation's first
+ // admission has no predecessor writer (holder.writer_box is
+ // empty before the first writer, claim rev 6.24): nothing is
+ // declared, since a declaration always names a box (INV-24)
lost = &writesLost{Box: p.Box, Gen: p.Gen, Session: p.Session, RemoteMaxTXID: a.RemoteMaxTXID}
e.Lost = lost
+ } else if a.Coverage == "" && !a.PredecessorCertified && p.Box < 0 {
+ w.stats["admission: no predecessor writer, nothing declared"]++
}
if n.Op.Kind == "restoring" && n.Op.LostAt != 0 && b.ID == n.Op.Src && on("writes-lost") {
// additional_writes_lost (claim rev 6.22 item 2, R13-3, R14-2):
@@ -1913,7 +1923,10 @@ func (w *world) admitCommit(b *box, c *claim, a admitInput) bool {
}
if lost != nil || len(lostTgt) > 0 {
// INV-21 (rev 7.15, R14-2): the declarations of one admission are
- // unique by (box, session), the ordinary record and the list together
+ // unique by (box, session), the ordinary record and the list together.
+ // INV-24 (claim rev 6.24, R16-3): every declaration names a box; the
+ // legacy identity, session 0, comes only with generation "" (@root),
+ // the adopted host's pre-claim writer, and is valid in both fields
seen := map[[2]int]bool{}
all := append([]*writesLost{}, lostTgt...)
if lost != nil {
@@ -1925,6 +1938,12 @@ func (w *world) admitCommit(b *box, c *claim, a admitInput) bool {
w.violate("INV-21: box %d declared {box %d, session %d} twice in one admission (writes_lost and additional_writes_lost)", b.ID, x.Box, x.Session)
}
seen[k] = true
+ if x.Box < 0 || x.Session == 0 && x.Gen != "" {
+ w.violate("INV-24: box %d declared {box %d, generation %q, session %d}: a declaration names a box, and session 0 is the legacy identity in the origin generation only", b.ID, x.Box, x.Gen, x.Session)
+ }
+ if x.Session == 0 {
+ w.stats["writes_lost: the legacy identity (session 0)"]++
+ }
}
}
return true
@@ -2171,6 +2190,25 @@ func (w *world) doObligation(b *box, c *claim, o obligation) {
// one of the grammar's four forms
w.violate("INV-23: chore %s (%s) carries a scope location %q outside the claim's grammar", o.ID, o.Kind, o.Scope.Location)
}
+ // membership (claim rev 6.24 §2, R16-1, R16-2): a park executes
+ // strictly within its scope's namespace. Every key the chore would
+ // archive must be in scope (INV-23's second half, checked on the
+ // manifest's sources), and under the rule the chore's listing is
+ // confined to the scope: the legacy instances/<host> scope excludes
+ // child incarnations, an instances scope never touches moves/, a
+ // staging lane's scope only its own lane. The mutation of
+ // scope-membership is rev 7.16's listing of the whole park prefix
+ var member func(string) bool
+ if o.Scope != nil && o.Kind != "park-local" {
+ loc := o.Scope.Location
+ member = func(k string) bool { return inScope(k, loc) }
+ for _, k := range w.buckets[b.Region].keys(o.Prefix) {
+ if !member(k) && hasNonMarker(w.buckets[b.Region].objs[k]) && (o.Kind != "park-sweep" || w.sweepable(w.claim, b.Region, k)) && !on("scope-membership") {
+ w.violate("INV-23: chore %s (%s, scope %s, incarnation %d) would archive %s, outside its scope", o.ID, o.Kind, loc, o.Scope.Inc, k)
+ break
+ }
+ }
+ }
if o.Kind == "park-replica" || o.Kind == "park-superseded" || o.Kind == "park-staging" {
// the owner first proves the region's box runs nothing for the
// host, when it is not the current executor (a whole-location
@@ -2244,8 +2282,12 @@ func (w *world) doObligation(b *box, c *claim, o obligation) {
w.violate("INV-22: remote chore %s (%s %s) is about to run with no persisted park scope", o.ID, o.Kind, o.Prefix)
}
var keep func(string) bool
+ if member != nil && on("scope-membership") {
+ keep = func(k string) bool { return !member(k) } // confined to the scope
+ }
if o.Kind == "park-sweep" {
- keep = func(k string) bool { return !w.sweepable(w.claim, b.Region, k) }
+ inner := keep
+ keep = func(k string) bool { return !w.sweepable(w.claim, b.Region, k) || inner != nil && inner(k) }
quiet = 0
if o.Scope != nil && on("quiet-before-ack") {
// the sweep's scope (claim rev 6.23 §6.2a): a park-sweep is a
@@ -2263,7 +2305,8 @@ func (w *world) doObligation(b *box, c *claim, o obligation) {
if retire == "@root" {
retire = "" // the origin generation's sentinel (claim rev 6.10)
}
- keep = func(k string) bool { return genOf(o.Prefix, k) != retire }
+ inner := keep
+ keep = func(k string) bool { return genOf(o.Prefix, k) != retire || inner != nil && inner(k) }
}
if !w.parkStep(b, o.Prefix, o.Dest(), quiet, keep) {
return
@@ -2322,6 +2365,11 @@ func (w *world) doObligation(b *box, c *claim, o obligation) {
func (o obligation) Dest() string {
if o.Kind == "park-sweep" {
+ if o.Scope != nil {
+ // the archive names the scope's incarnation (claim rev 6.24 §2;
+ // R16-2: rev 7.16's destination ignored it)
+ return fmt.Sprintf("parked/sweep/%s/%d/%s/", host, o.Scope.Inc, o.ID)
+ }
return fmt.Sprintf("parked/sweep/%s/%s/", host, o.ID)
}
if o.Kind == "park-replica" {
@@ -2351,6 +2399,48 @@ func (w *world) scoped(o obligation, evidenceAt, inc int) obligation {
// scopeLocation is the scope grammar's form of a park prefix.
func scopeLocation(prefix string) string { return strings.TrimSuffix(prefix, "/") }
+// scopeOf is the scope location a key belongs to (claim rev 6.24 §2): a
+// staging key's is moves/<host> (or its own lane, moves/<host>/<op id>,
+// which inScope tests by prefix); an instance key's is its incarnation
+// location, or the legacy instances/<host> for a level directory or
+// generation directly under the host prefix. A child incarnation never
+// belongs to the legacy scope, and nothing under moves/ belongs to an
+// instances scope.
+func scopeOf(k string) string {
+ if strings.HasPrefix(k, "moves/"+host+"/") {
+ return "moves/" + host
+ }
+ return scopeLocation(locationOf(k))
+}
+
+// inScope: key k is within the scope location loc.
+func inScope(k, loc string) bool {
+ if strings.HasPrefix(loc, "moves/"+host+"/") {
+ return strings.HasPrefix(k, loc+"/") // one staging lane
+ }
+ return scopeOf(k) == loc
+}
+
+// incOf is the archive incarnation of a scope location (claim rev 6.24
+// §2): an incarnation location's own; for the legacy location or the
+// staging sweep, the holder's incarnation, else the newest retired.
+func incOf(c *claim, loc string) int {
+ if strings.HasPrefix(loc, "instances/"+host+"/") {
+ n := 0
+ fmt.Sscanf(strings.TrimPrefix(loc, "instances/"+host+"/"), "%d", &n)
+ if n > 0 {
+ return n
+ }
+ }
+ if c.Holder != nil {
+ return c.Holder.Inc
+ }
+ if len(c.Retired) > 0 {
+ return c.Retired[len(c.Retired)-1]
+ }
+ return 0
+}
+
// scopeLocationRe is claim rev 6.23's grammar for a scope's location
// (the model's incarnation ids are decimal, the claim's 64 hex): exactly
// instances/<host>, instances/<host>/<incarnation>, moves/<host>/<op id>
diff --git a/internal/sim/hostlife/rules_test.go b/internal/sim/hostlife/rules_test.go
index c84baa823..3efd01d9d 100644
--- a/internal/sim/hostlife/rules_test.go
+++ b/internal/sim/hostlife/rules_test.go
@@ -130,6 +130,7 @@ var allRules = []string{
"covered-baseline-refused", // claim rev 6.22 §6.2a: walking an unfinished run, a covered foreign link is passed over and a covered local generation reached as a needed baseline refuses the start (R13-1)
"restore-selected-baseline", // rev 7.15 §5.1: the start restores the generation the run walk selected, rooted at [1, 1], as the oracle does; not the marker's alone (R14-1)
"unique-declarations", // claim rev 6.22 item 2: writes_lost plus additional_writes_lost, unique by (box, session); the source's cut session joins the list and is never declared twice (R14-2)
+ "scope-membership", // claim rev 6.24 §2: a park executes strictly within its scope's namespace; the legacy instances/<host> scope excludes child incarnations and an instances scope never touches moves/ (R16-1, R16-2)
"lost-return-park", // rev 7 §7.10 (model 27): declare-lost parks a return lane the lost target had begun (rev 7.2: a biased schedule stops the target mid-return)
"target-park", // R2-5/INV-10
"abort-target-owner", // J-N13
diff --git a/internal/sim/hostlife/sim_test.go b/internal/sim/hostlife/sim_test.go
index c405e0cac..1cac18606 100644
--- a/internal/sim/hostlife/sim_test.go
+++ b/internal/sim/hostlife/sim_test.go
@@ -88,7 +88,7 @@ func (w *world) legacyStart() {
b.DB = &localDB{Inc: inc, Loc: locationFor(inc, true), Key: key, State: map[string]int{}, Epoch: b.Epoch}
for i := 0; i < 3; i++ {
w.nextW++
- w.acks = append(w.acks, ack{W: w.nextW, Inc: inc, Box: b.ID, Session: 1, Gen: "", At: 0, Kind: "write"}) // the pre-release writer: adoption's session 1
+ w.acks = append(w.acks, ack{W: w.nextW, Inc: inc, Box: b.ID, Session: 0, Gen: "", At: 0, Kind: "write"}) // the pre-release writer: the legacy identity, session 0 (claim rev 6.24)
w.writeInc[w.nextW] = inc
w.commit(b.DB, w.nextW)
}
@@ -107,8 +107,8 @@ func (w *world) legacyStart() {
other := (r + 1 + w.rng.Intn(nRegions-1)) % nRegions
w.buckets[other].objs[ltxKey(b.DB.Loc, "", 0, 1, 1)] = []*version{{ID: w.newVer(), B: part}}
if w.rng.Intn(2) == 0 {
- // the pre-release processes: adoption's session 1, the origin generation (rev 7.13; seed 44450 / 100,000 acknowledged under no session)
- b.Procs = []*proc{{Main: true, Strict: false, Inc: inc, Session: 1, Box: b.ID}, {Main: false, Strict: false, Inc: inc, Session: 1, Box: b.ID}}
+ // the pre-release processes: the legacy identity, session 0 in the origin generation (claim rev 6.24; rev 7.13's seed 44450 found them acknowledging under no identity at all)
+ b.Procs = []*proc{{Main: true, Strict: false, Inc: inc, Session: 0, Box: b.ID}, {Main: false, Strict: false, Inc: inc, Session: 0, Box: b.ID}}
}
if w.rng.Intn(2) == 0 {
// that earlier move also left the old box's local state behind:
diff --git a/internal/sim/hostlife/witness_test.go b/internal/sim/hostlife/witness_test.go
index b6773cbae..3ff197598 100644
--- a/internal/sim/hostlife/witness_test.go
+++ b/internal/sim/hostlife/witness_test.go
@@ -169,6 +169,8 @@ var witnesses = []witness{
"a committed move's source-region park binds its scope, the original evidence time, in the claim; the region's agent restarts mid-park; with the box-local evidence map (rev 7.13) the horizon starts again from the restart and the acknowledgement comes late (claim rev 6.15 §2, R13-4)", wParkScopeSurvivesRestart},
{"persisted-park-scope", "persisted-park-scope", "INV-9",
"a committed move's staging lane sealed at the cut: its persisted bound is the seal's, deadline seal + lease + H; the target region's cleanup first runs 40 steps after that deadline and its agent restarts as it begins; the acknowledgement must follow at once, since the persisted deadline has passed; bound at cleanup's own clock (rev 7.15) the chore waits a fresh horizon (claim rev 6.23 §2, R15-1)", wStagingScopeDeadline},
+ {"scope-membership", "scope-membership", "INV-23",
+ "incarnation 2 serves after incarnation 1 was deleted; a late object lands under instances/<host>/1/g-old/; the sweep must queue a chore scoped to instances/<host>/1 with archive incarnation 1 and park within it; rev 7.16's sweep scoped the whole prefix as instances/<host> under incarnation 2 and archived the child incarnation's object from there (claim rev 6.24 §2, R16-2)", wSweepScopeMembership},
{"holderless-park", "holderless-park", "INV-20",
"a deleted host's region keeps late residue; the free host's sweep runs as a holderless parking op and a create requested meanwhile waits for FreeAfterPark; with bare chores on the free claim (rev 7.13) the create claims the host over the outstanding park (claim rev 6.15 §6.2a, R13-4)", wFreeSweepVsCreate},
{"covered-baseline-refused", "covered-baseline-refused", "INV-2",
@@ -416,11 +418,11 @@ func (w *world) legacy(b *box, other int) {
b.DB = &localDB{Inc: inc, Loc: locationFor(inc, true), Key: key, State: map[string]int{}, Epoch: b.Epoch}
for i := 0; i < 2; i++ {
w.nextW++
- w.acks = append(w.acks, ack{W: w.nextW, Inc: inc, Box: b.ID, Session: 1, At: w.now, Kind: "write"}) // the pre-release writer: adoption's session 1
+ w.acks = append(w.acks, ack{W: w.nextW, Inc: inc, Box: b.ID, Session: 0, At: w.now, Kind: "write"}) // the pre-release writer: the legacy identity, session 0 (claim rev 6.24)
w.writeInc[w.nextW] = inc
w.commit(b.DB, w.nextW)
}
- b.DB.Session = 1
+ b.DB.Session = 0
b.Files = &filesDir{Inc: inc, Key: key, Env: 1, Done: true}
b.Config, b.CfgVer, b.Live, b.LiveInc = inc, 1, app0, inc
full := w.deltaOf(b.DB, 0)
@@ -428,9 +430,11 @@ func (w *world) legacy(b *box, other int) {
k := ltxKey(b.DB.Loc, "", 0, 1, 1)
w.buckets[b.Region].objs[k] = []*version{{ID: w.newVer(), B: full}}
w.markDurable(k, full)
- // the pre-release writer closed cleanly on this box; adoption's stamp
- // and session are 1
- w.buckets[b.Region].objs[closedKey(b.DB.Loc, "", 1)] = []*version{{ID: w.newVer(), B: &blob{Kind: "closed", Inc: inc, Stamp: 1, Seq: b.ID, Session: 1, TX: 1}}}
+ // the pre-release writer closed cleanly on this box (a fixture for a
+ // warm first wake; the real pre-release writer leaves no certificate,
+ // so a real first wake is cold); adoption's stamp is 1 and the legacy
+ // identity's session 0 (claim rev 6.24)
+ w.buckets[b.Region].objs[closedKey(b.DB.Loc, "", 0)] = []*version{{ID: w.newVer(), B: &blob{Kind: "closed", Inc: inc, Stamp: 1, Seq: b.ID, Session: 0, TX: 1}}}
first := w.txs[b.DB.Log[0]]
part := &blob{Kind: "ltx", Level: 0, Min: 1, Max: 1, Inc: 0, SealKey: key, Pages: map[string]int{first.Key: first.Val}, TXs: []int{first.W}}
w.buckets[other].objs[ltxKey(b.DB.Loc, "", 0, 1, 1)] = []*version{{ID: w.newVer(), B: part}}
@@ -2271,7 +2275,7 @@ func wAnchorRetry(t *testing.T, w *world) {
k := snapKey(loc, "", 2)
w.buckets[b.Region].objs[k] = append(w.buckets[b.Region].objs[k], &version{ID: w.newVer(), B: snap})
w.markDurable(k, snap)
- w.buckets[b.Region].objs[closedKey(loc, "", 1)][0].B.TX = 2 // the certificate names the snapshot's position
+ w.buckets[b.Region].objs[closedKey(loc, "", w.claim.Holder.Session)][0].B.TX = 2 // the certificate names the snapshot's position (the serving fixture's session, not the legacy identity)
var original *request
w.hold = func(r *request) bool {
if r.kind == rPut && r.b != nil && r.b.Level == 0 && r.b.Min == 2 && r.b.Max == 2 && original == nil {
@@ -2397,7 +2401,7 @@ func wOriginSwept(t *testing.T, w *world) {
}
// the pre-release writer left no certificate (the helper's is for a
// warm first wake): the first wake is cold, from the origin
- delete(w.buckets[b0.Region].objs, closedKey(w.claim.Holder.Location, "", 1))
+ delete(w.buckets[b0.Region].objs, closedKey(w.claim.Holder.Location, "", 0))
armed := true
w.startHook = func(x *box) bool {
if x != b0 || !armed {
@@ -2552,6 +2556,181 @@ func wStagingScopeDeadline(t *testing.T, w *world) {
w.finish(t)
}
+// ---------- rev 7.17 ----------
+
+// recreated drives a delete of the serving host and a create of the next
+// incarnation in region r, and returns once it serves.
+func (w *world) recreated(t *testing.T, r int) {
+ w.userDelete()
+ w.reach(t, 800, "host deleted and free", func() bool {
+ c := w.claim
+ return c != nil && c.Holder == nil && c.Op.Kind == "" && len(c.Obl) == 0 && w.rec == nil
+ }, w.actAll(), w.runConsole, w.recoveryPass)
+ if len(w.violations) > 0 {
+ return
+ }
+ w.nextInc++
+ w.console = append(w.console, &consolePlan{Kind: "create", Host: "h", App: app0, OpID: w.newID("cr"), Inc: w.nextInc, Region: r})
+ w.reach(t, 800, "the next incarnation serves", func() bool {
+ c := w.claim
+ return len(w.violations) > 0 || c != nil && c.Holder != nil && c.Op.Kind == "serving" && c.Holder.Inc == w.nextInc
+ }, w.actAll(), w.runConsole, w.recoveryPass)
+}
+
+// wSweepScopeMembership (claim rev 6.24 §2, R16-2): the serving
+// incarnation's sweep finds a late object of a deleted incarnation; the
+// chore that parks it is scoped to that incarnation's own location and
+// archives under its incarnation, never under the serving one's legacy
+// scope.
+func wSweepScopeMembership(t *testing.T, w *world) {
+ b0 := w.boxes[0]
+ w.serve(b0, 1, app0)
+ w.wake(b0)
+ w.appWrite(b0)
+ w.recreated(t, b0.Region)
+ if len(w.violations) > 0 {
+ return
+ }
+ if w.claim.Holder.Inc != 2 {
+ t.Fatalf("witness: incarnation %d serves, want 2: %s", w.claim.Holder.Inc, w.state())
+ }
+ // a late object of incarnation 1 lands under its own location
+ old := locationFor(1, false)
+ k := ltxKey(old, "old", 0, 3, 3)
+ w.buckets[b0.Region].objs[k] = append(w.buckets[b0.Region].objs[k], &version{ID: w.newVer(), B: &blob{Kind: "ltx", Level: 0, Min: 3, Max: 3, Inc: 1, Pages: map[string]int{}}})
+ var scope *parkScope
+ w.reach(t, 600, "the late object parked", func() bool {
+ for _, o := range w.claim.Obl {
+ if o.Kind == "park-sweep" && o.Region == b0.Region && o.Scope != nil && strings.HasPrefix(k, o.Prefix) {
+ cp := *o.Scope
+ scope = &cp
+ }
+ }
+ return len(w.violations) > 0 || w.buckets[b0.Region].current(k) == nil
+ }, w.actAll(), w.recoveryPass)
+ if len(w.violations) > 0 {
+ return
+ }
+ if scope == nil {
+ t.Fatalf("witness: no scoped sweep chore covered %s: %s", k, w.state())
+ }
+ if scope.Location != scopeLocation(old) || scope.Inc != 1 {
+ w.violate("INV-23 (witness): the sweep parked %s under scope %s (incarnation %d); the object's own location is %s, incarnation 1", k, scope.Location, scope.Inc, scopeLocation(old))
+ return
+ }
+ w.finish(t)
+}
+
+// TestLegacyLostTargetReturn (claim rev 6.24, R16-3) is the legacy
+// identity through a lost-target return: an adopted host whose pre-claim
+// writer (session 0, generation "") left a write its replica never got;
+// no wake before the move, so the cut session is 0 and src_covered is
+// false; the target is admitted cold and declared lost; the source's
+// return declares the target's session and the source's cut session 0, in
+// additional_writes_lost, and INV-24 accepts the legacy identity.
+func TestLegacyLostTargetReturn(t *testing.T) {
+ w := newWitness()
+ defer func() {
+ if t.Failed() {
+ dump(t, w)
+ }
+ }()
+ b0, b1 := w.boxes[0], w.boxes[1]
+ w.legacy(b0, 1)
+ w.tick(func() { w.reconcile(b0) }) // adoption
+ if w.claim == nil || w.claim.Holder.Session != 0 {
+ t.Fatalf("not adopted with the legacy identity: %s", w.state())
+ }
+ w.reach(t, 400, "adoption settled", func() bool { return w.claim.oblFor("adopt") == 0 }, w.actAll(), w.recoveryPass)
+ // the pre-claim writer's last write, acknowledged, never uploaded: the
+ // origin's level-0 uploads are held, so the cut's flush cannot land it
+ loc := w.claim.Holder.Location
+ w.hold = func(r *request) bool {
+ return r.kind == rPut && r.region == b0.Region && r.b != nil && r.b.Level == 0 && genOf(loc, r.key) == ""
+ }
+ w.nextW++
+ w.commit(b0.DB, w.nextW)
+ w.acks = append(w.acks, ack{W: w.nextW, Inc: 1, Box: b0.ID, Session: 0, Gen: "", At: w.now, Kind: "write"})
+ w.writeInc[w.nextW] = 1
+ w.moveTo(b1.Region)
+ w.reach(t, 600, "landing probed", func() bool {
+ c := w.claim
+ return len(w.violations) > 0 || c.Op.Kind == "landing" && c.Op.LandPhase == "probed" && hasMain(b1)
+ }, w.act(b0, b1), w.recoveryPass)
+ if len(w.violations) > 0 {
+ t.Fatalf("%s", w.violations[0])
+ }
+ if w.claim.Op.SrcCovered || w.claim.Op.CutSession != 0 {
+ t.Fatalf("the cut must be session 0 with src_covered false: covered=%v cut=%d", w.claim.Op.SrcCovered, w.claim.Op.CutSession)
+ }
+ tgtSession := lastAdmission(w.claim.Holder).Session
+ w.stop(b1)
+ w.reach(t, 400, "declared lost", func() bool { return contains(w.claim.Lost, b1.ID) }, w.act(b0), w.recoveryPass, w.operator)
+ w.replaceDestroyed()
+ w.reach(t, 800, "the source's return admitted", func() bool {
+ c := w.claim
+ l := lastAdmission(c.Holder)
+ return len(w.violations) > 0 || c.Op.Kind == "serving" || c.Op.Kind == "restoring" && l != nil && l.Box == b0.ID && l.Session > tgtSession
+ }, w.act(b0), w.recoveryPass, w.runConsole, w.operator)
+ w.hold = nil
+ w.completeHeld(dataKey(loc, "", ""))
+ if len(w.violations) > 0 {
+ t.Fatalf("%s", w.violations[0])
+ }
+ e := lastAdmission(w.claim.Holder)
+ seen := map[[2]int]int{}
+ for _, x := range declarationsOf(e) {
+ seen[[2]int{x.Box, x.Session}]++
+ }
+ if seen[[2]int{b1.ID, tgtSession}] != 1 || seen[[2]int{b0.ID, 0}] != 1 {
+ t.Fatalf("the return must declare the target's session %d and the source's cut session 0 once each: %v (record %+v)", tgtSession, seen, *e)
+ }
+ t.Logf("declared %v at %s; legacy declarations %d", seen, loc, w.stats["writes_lost: the legacy identity (session 0)"])
+ w.finish(t)
+ if len(w.violations) > 0 {
+ t.Fatalf("%s", w.violations[0])
+ }
+}
+
+// TestStagingSurvivesLegacyPark (claim rev 6.24 §2, R16-1): a legacy host's
+// earlier stay in the target region is parked by the landing's install
+// phase while the move's sealed staging sits beside it under moves/; the
+// park is scoped to the legacy instance location and the staging survives
+// for the build.
+func TestStagingSurvivesLegacyPark(t *testing.T) {
+ w := newWitness()
+ defer func() {
+ if t.Failed() {
+ dump(t, w)
+ }
+ }()
+ b0, b1 := w.boxes[0], w.boxes[1]
+ w.legacy(b0, 1) // the fixture leaves an earlier stay's replica part in region 1
+ w.tick(func() { w.reconcile(b0) })
+ w.reach(t, 400, "adoption settled", func() bool { return w.claim.oblFor("adopt") == 0 }, w.actAll(), w.recoveryPass)
+ w.recovered(t, b0, "running after adoption")
+ w.appWrite(b0)
+ w.moveTo(b1.Region)
+ w.reach(t, 600, "landing probed", func() bool {
+ c := w.claim
+ return len(w.violations) > 0 || c.Op.Kind == "landing" && c.Op.LandPhase == "probed" && hasMain(b1)
+ }, w.act(b0, b1), w.recoveryPass)
+ if len(w.violations) > 0 {
+ t.Fatalf("%s", w.violations[0])
+ }
+ op := w.claim.Op
+ for _, r := range []ref{op.SealDB, op.SealFiles} {
+ if w.buckets[r.Region].byID(r.Key, r.Ver) == nil || w.buckets[r.Region].current(r.Key) == nil {
+ t.Fatalf("the sealed staging %s@%d is gone from region %d after the install park", r.Key, r.Ver, r.Region)
+ }
+ }
+ t.Logf("staging %s and %s survived the legacy instance park; landing probed", op.SealDB.Key, op.SealFiles.Key)
+ w.finish(t)
+ if len(w.violations) > 0 {
+ t.Fatalf("%s", w.violations[0])
+ }
+}
+
// wFreeSweepVsCreate (claim rev 6.15 §6.2a, R13-4): a deleted host's
// region keeps late residue; the free host's sweep runs as a holderless
// parking op, and a create requested meanwhile waits for FreeAfterPark.