hostclaim: naming a converted move's same-region target takes the move's own target off the barrier in the same CAS
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2 files changed,
+33
−1
internal/hostclaim/relocation.go+8 −1internal/hostclaim/relocation_test.go+25 −0
diff --git a/internal/hostclaim/relocation.go b/internal/hostclaim/relocation.go| index c425d6350..5238422c9 100644 |
| --- a/internal/hostclaim/relocation.go |
| +++ b/internal/hostclaim/relocation.go |
| @@ -742,7 +742,11 @@ func JournalPin(opID string, p RecoveryPin) func(*Claim) error { |
| // NameTarget names the target of an operation that has none: a move |
| // converted to relocating before its flip (claim §4, moves §7.10). The |
| -// named box acknowledges (D6) before ToInstalling hands it op.exec. |
| +// named box acknowledges (D6) before ToInstalling hands it op.exec. The |
| +// move's own target (move_target_box, in the region the move was going |
| +// to) leaves the barrier in the same CAS: readiness binds to the box named |
| +// here (moves §7.10), and a failover's participant is its target alone |
| +// (§5.8). It never held op.exec on the converted operation. |
| func NameTarget(opID string, t Target) func(*Claim) error { |
| return func(c *Claim) error { |
| if err := requireRelocation(c, opID, PhasePinning); err != nil { |
| @@ -751,6 +755,9 @@ func NameTarget(opID string, t Target) func(*Claim) error { |
| if c.Op.Intent["target_box"] != "" { |
| return fmt.Errorf("%w: the operation already has a target", ErrRelocation) |
| } |
| + if moved := c.Op.Intent["move_target_box"]; moved != "" { |
| + removeParticipant(c, moved) |
| + } |
| return replaceTargetSlot(c, t) |
| } |
| } |
diff --git a/internal/hostclaim/relocation_test.go b/internal/hostclaim/relocation_test.go| index 8a1a41055..98bf47e02 100644 |
| --- a/internal/hostclaim/relocation_test.go |
| +++ b/internal/hostclaim/relocation_test.go |
| @@ -1135,3 +1135,28 @@ func TestRelocationConvertedMovePinsOnlyAfterFenceSourceAndTheHold(t *testing.T) |
| t.Fatalf("pin not journaled: %+v", o.Claim.Op.Journal) |
| } |
| } |
| + |
| +// Task 5 fix round 1: a converted move's own target (in the region the move |
| +// was going to) leaves the barrier when NameTarget names the same-region |
| +// target, in the same CAS. Otherwise a move target that never acknowledged, |
| +// or died, holds the relocation in pinning for ever: nothing in this |
| +// region can consume its fence (moves §7.10, §5.8's failover participants). |
| +func TestRelocationNameTargetDropsTheConvertedMovesTarget(t *testing.T) { |
| + ctx := context.Background() |
| + s, o, ref, a := c7Fixture(t) |
| + o, err := ConvertMovingToRelocating(ctx, s, o, "move-1", ref, a, &c7Guard{}) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if !slices.Equal(o.Claim.Op.Participants, []string{"bri-knitting"}) || o.Claim.Op.Intent["move_target_box"] != "bri-knitting" { |
| + t.Fatalf("not the converted shape this test is about: %+v", o.Claim.Op) |
| + } |
| + gen := o.Claim.Gen |
| + o = step(t, s, o, NameTarget("move-1", Target{Box: "cai-knitting", Enrollment: strings.Repeat("e", 32)})) |
| + if !slices.Equal(o.Claim.Op.Participants, []string{"cai-knitting"}) || o.Claim.Op.Intent["target_box"] != "cai-knitting" || o.Claim.Gen != gen+1 { |
| + t.Fatalf("the move's target is still on the barrier: %+v", o.Claim.Op) |
| + } |
| + if o.Claim.Op.Intent["move_target_box"] != "bri-knitting" || o.Claim.Op.Journal["replacements"] != "" { |
| + t.Fatalf("the move's history or the replacement count changed: %+v", o.Claim.Op) |
| + } |
| +} |