carlos / platform Public

sim: naming a converted move's same-region target takes the move's own target off the participants in the same CAS (rule reloc-name-drops-move-target, mirroring 1b-next 151261fe); a stopped move target no longer holds the relocation at pinning

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Paul Campbell pushed by claude agent, operated by paul@keymail.dev 66379fb3121648e50ab69085128b559f396092ee parent 04c9487
5 files changed, +94 −3
  • internal/sim/hostlife/regionha_round1_test.go +29 −0
  • internal/sim/hostlife/regionha_test.go +27 −3
  • internal/sim/hostlife/regionha_witness_test.go +34 −0
  • internal/sim/hostlife/rules_test.go +1 −0
  • internal/sim/hostlife/witness_test.go +3 −0
diff --git a/internal/sim/hostlife/regionha_round1_test.go b/internal/sim/hostlife/regionha_round1_test.go
index 59c8f7bb0..bc243e370 100644
--- a/internal/sim/hostlife/regionha_round1_test.go
+++ b/internal/sim/hostlife/regionha_round1_test.go
@@ -605,6 +605,35 @@ func TestConvertedMoveWaitsForATargetBeforeItsPin(t *testing.T) {
w.mustHold(t)
}
+// TestConvertedMoveDropsTheMoveTargetFromTheBarrier
+// (reloc-name-drops-move-target; final review Important 1, 1b-next
+// 151261fe): the move's source is terminated before the flip, and the
+// move's own target in region 1, the one participant the conversion
+// left, is stopped before it acknowledges. Naming the same-region target
+// takes the move's target off the participants in the same CAS, and the
+// relocation reaches installing and serves while that box stays
+// stopped. The mutation keeps it on the barrier: the relocation waits at
+// pinning for ever.
+func TestConvertedMoveDropsTheMoveTargetFromTheBarrier(t *testing.T) {
+ w := twoBoxWitness()
+ mt := w.convertedMoveTargetStopped(t)
+ if !w.reach(t, 400, "the target named", func() bool { return w.claim.Op.Tgt >= 0 }, w.actAll(), w.recoveryPass) {
+ w.mustHold(t)
+ }
+ if c := w.claim; contains(c.Op.Parts, mt.ID) || len(c.Op.Parts) != 1 || c.Op.Parts[0] != c.Op.Tgt {
+ t.Fatalf("the move's target %d is still on the barrier: parts=%v target=%d", mt.ID, c.Op.Parts, c.Op.Tgt)
+ }
+ if !w.relocPhase(t, "installing", w.actAll(), w.recoveryPass) {
+ w.mustHold(t)
+ }
+ if !mt.Destroyed {
+ t.Fatalf("the move's target started again")
+ }
+ w.servingOn(t, nil)
+ w.finish(t)
+ w.mustHold(t)
+}
+
// TestFailoverStartWaitsForAdmission (reloc-failover-start-guard; rev
// 7, Astra round 4 finding 3): with the app's admission closed, a
// terminated serving executor's failover does not start (Worker.Start's
diff --git a/internal/sim/hostlife/regionha_test.go b/internal/sim/hostlife/regionha_test.go
index ee54ac62d..b13fcb347 100644
--- a/internal/sim/hostlife/regionha_test.go
+++ b/internal/sim/hostlife/regionha_test.go
@@ -810,20 +810,44 @@ func (w *world) relocPinning(b *box, c *claim) {
}
// relocNameTarget is NameTarget at pinning: choose() in the source region
-// over the eligible boxes, the excluded ones left out.
+// over the eligible boxes, the excluded ones left out. A converted move's
+// own target (1b-next's move_target_box, in the region the move was
+// going to) leaves the participants in the same CAS (rule
+// reloc-name-drops-move-target, 1b-next 151261fe): readiness binds to the
+// box named here (moves §7.10) and a failover's participant is its
+// target alone (§5.8); the move's target never held op.exec on the
+// converted operation. A relocation's participants are otherwise all in
+// its region, so the model drops every participant outside op.SrcRegion,
+// which only a converted move leaves there. The mutation keeps it, and
+// the barrier waits for a box that may never acknowledge: a stopped move
+// target holds the relocation at pinning with no deadline and no stuck
+// (INV-3; TestConvertedMoveDropsTheMoveTargetFromTheBarrier).
func (w *world) relocNameTarget(c *claim) {
op := c.Op
to := w.chooseBoxExcept(op.SrcRegion, c.Holder.App, relocExcept(op)...)
if to == nil {
return // no eligible box: the recovery pass alerts once (moves §7.10)
}
- w.cas(c, func(n *claim) bool {
+ dropped := false
+ if w.cas(c, func(n *claim) bool {
ok := sameOp(n.Op.ID, op.ID) && n.Op.Kind == "relocating" && n.Op.Phase == "pinning" && n.Op.Tgt < 0
+ if ok && on("reloc-name-drops-move-target") {
+ var parts []int
+ for _, p := range n.Op.Parts {
+ if x := w.boxByID(p); x == nil || x.Region == n.Op.SrcRegion {
+ parts = append(parts, p)
+ }
+ }
+ dropped = len(parts) < len(n.Op.Parts)
+ n.Op.Parts = parts
+ }
if ok {
ok = w.nameReplacement(n, to.ID, "pinning")
}
return ok
- })
+ }) && dropped {
+ w.stats["name target: the move's target left the participants"]++
+ }
}
// abandonPrepared is abandonPrepared(c) in hostclaim (1b-next ruling
diff --git a/internal/sim/hostlife/regionha_witness_test.go b/internal/sim/hostlife/regionha_witness_test.go
index a1a955d8e..4195f2778 100644
--- a/internal/sim/hostlife/regionha_witness_test.go
+++ b/internal/sim/hostlife/regionha_witness_test.go
@@ -965,3 +965,37 @@ func wPushKeepsLiveFiles(t *testing.T, w *world) {
w.servingOn(t, w.boxes[3])
w.finish(t)
}
+
+// convertedMoveTargetStopped converts a move (convertedMove: the source,
+// box 0, is terminated before the flip) and stops the move's own target,
+// the one participant the conversion left, before it acknowledges; it
+// returns that box. Stopped is not terminated: nothing fences it, and
+// it never acknowledges the converted operation.
+func (w *world) convertedMoveTargetStopped(t *testing.T) *box {
+ w.convertedMove(t, nil)
+ c := w.claim
+ if len(c.Op.Parts) != 1 || len(c.Op.Acks) != 0 || c.Op.Tgt >= 0 {
+ t.Fatalf("not a converted move whose one participant is the move's target: %s", w.state())
+ }
+ mt := w.boxByID(c.Op.Parts[0])
+ if mt == nil || mt.Region == c.Op.SrcRegion {
+ t.Fatalf("the converted move's participant %d is not the move's target in another region: %s", c.Op.Parts[0], w.state())
+ }
+ w.stop(mt)
+ return mt
+}
+
+// wConvertedMoveTargetStopped (reloc-name-drops-move-target; final
+// review Important 1, 1b-next 151261fe): a move's source is terminated
+// before the flip, the move converts to a relocation at pinning, and the
+// move's target in region 1 is stopped before it acknowledges. Naming
+// the region's other box takes the move's target off the barrier in the
+// same CAS, and the host serves on the named box; without the rule the
+// barrier waits for the stopped box, with no deadline and no stuck, and
+// nothing moves (INV-3).
+func wConvertedMoveTargetStopped(t *testing.T, w *world) {
+ w.twoBox()
+ w.convertedMoveTargetStopped(t)
+ w.servingOn(t, nil)
+ w.finish(t)
+}
diff --git a/internal/sim/hostlife/rules_test.go b/internal/sim/hostlife/rules_test.go
index eef576edf..892fdf82a 100644
--- a/internal/sim/hostlife/rules_test.go
+++ b/internal/sim/hostlife/rules_test.go
@@ -204,4 +204,5 @@ var allRules = []string{
"reloc-name-before-pin", // rev 7 (Astra round 4 finding 1; 1b-next rev 19 Worker.pinning): a relocation at pinning with no target names one before it selects a pin, on every path; the mutation pins first, and a manifest that lands while no box is eligible is left out (a loss bound: TestConvertedMoveWaitsForATargetBeforeItsPin)
"reloc-failover-start-guard", // rev 7 (Astra round 4 finding 3; 1b-next rev 19 Worker.Start, StartRelocation): a serving executor's failover starts only with an eligible target, the app admitting the registration (and its recheck) and RelocationStartGuard with the source fenced; the mutation starts at once (TestFailoverStartWaitsForAdmission, TestFailoverStartWaitsForParkReplica)
"reloc-failover-ack-before-pin", // rev 5 and 6 (Astra rounds 2 and 3; 1b-next rev 19 PinFailover): an initial failover (op.PinFailover, the model's starting) selects its pin only after the target is named and every participant acknowledged; converted moves, nested failovers and replays pin first, as Worker.pinning does; the mutation pins first, and a manifest that lands while the target's controller is unavailable is left out (a loss bound: TestFailoverPinWaitsForTheAcknowledgement)
+ "reloc-name-drops-move-target", // final review Important 1 (1b-next 151261fe, NameTarget; moves §7.10, region HA §5.8): naming a converted move's same-region target takes the move's own target (move_target_box, in the other region) off the participants in the same CAS; the mutation keeps it on the barrier, and a stopped move target holds the relocation at pinning for ever (INV-3; TestConvertedMoveDropsTheMoveTargetFromTheBarrier, witness)
}
diff --git a/internal/sim/hostlife/witness_test.go b/internal/sim/hostlife/witness_test.go
index a93c223cf..96d461349 100644
--- a/internal/sim/hostlife/witness_test.go
+++ b/internal/sim/hostlife/witness_test.go
@@ -258,6 +258,9 @@ var witnesses = []witness{
"F1: a drain's target fails its probe in a two-box region with no spare; with the bound the operation is stuck and the operator's resolve returns the host to the source, without it it waits for ever", wNoSpareBound},
{"reloc-dead-target-bound", "reloc-dead-target-bound", "INV-3",
"rev 18 finding 2: the target dies at installing and no spare is launched; with the bound the fence is consumed into stuck and the operator's replay serves on the source, without it nothing moves", wDeadTargetBound},
+ // the final review's fixes
+ {"reloc-name-drops-move-target", "reloc-name-drops-move-target", "INV-3",
+ "1b-next 151261fe: a move converts to a relocation when its source is terminated before the flip, and the move's target in the other region is stopped before it acknowledges; naming the region's other box takes the move's target off the barrier and the host serves there, without the rule the barrier waits for the stopped box for ever", wConvertedMoveTargetStopped},
}
// TestWitnesses is the deterministic half of the mutation evidence.