carlos / platform Public

host: moves evidence fix round 2 (the writer's lease is not residue)

retiredGenerations classified <holder.location>/lock.json, the writer's
lease that stands at every live location, as an object no generation
owns, so every live host with a lease was refused.

The provider now skips exactly the lock objects the park engine skips:
movesParkLock mirrors park's unexported isLock over locationOf (the
lock.json at the root of its own park location), and also replaces the
moves/<host> root special case. TestMovesParkLockMatchesTheEngine keeps
the mirror in step with park.Decide as the oracle;
TestLiveLocationLeaseIsSweptNormally sweeps a live location holding the
lease, scoped and bare.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev c35a1cccd420be11cd048940206bc992fbdbd6db parent 7e94ea3
2 files changed, +93 −5
  • internal/host/movesevidence.go +26 −5
  • internal/host/movesevidence_test.go +67 −0
diff --git a/internal/host/movesevidence.go b/internal/host/movesevidence.go
index 149392519..20d7ee717 100644
--- a/internal/host/movesevidence.go
+++ b/internal/host/movesevidence.go
@@ -157,7 +157,9 @@ func (e movesRetirementEvidence) VerifyRetirement(ctx context.Context, o hostcla
// order and carry no time), and every version of it must predate the close.
// A rolled-back or aborted move, or a landing that admitted a fresh name,
// leaves no such admission, so no generation is vouched for. Objects are
-// classified as the landing's view classifies them (landingGenerationOf).
+// classified as the landing's view classifies them (landingGenerationOf),
+// after the lock objects the engine itself skips (movesParkLock, fix round
+// 2): the writer's lease stands at every live location.
func (r movesRetired) retiredGenerations(versions []lease.ObjectVersion) error {
h := r.holder
refuse := func(gen, format string, args ...any) error {
@@ -177,7 +179,7 @@ func (r movesRetired) retiredGenerations(versions []lease.ObjectVersion) error {
}
}
for _, v := range versions {
- if loc, ok := movesInstancesLocation(r.host, v.Key); !ok || loc != r.location {
+ if loc, ok := movesInstancesLocation(r.host, v.Key); !ok || loc != r.location || movesParkLock(r.host, v.Key) {
continue
}
gen, ok := landingGenerationOf(r.location, v.Key)
@@ -276,11 +278,11 @@ func (e movesRetirementEvidence) retired(ctx context.Context, o hostclaim.Observ
if !ok {
return refuse(true, "the census holds %s, outside this host", v.Key)
}
+ if movesParkLock(host, v.Key) {
+ continue // the moves/<host> park's own lease
+ }
op, _, inLane := strings.Cut(tail, "/")
if !inLane {
- if tail == "lock.json" {
- continue // the moves/<host> park's own lease
- }
return refuse(true, "%s is outside every lane", v.Key)
}
if op == last.ID {
@@ -292,3 +294,22 @@ func (e movesRetirementEvidence) retired(ctx context.Context, o hostclaim.Observ
}
return movesRetired{host: host, incarnation: h.Incarnation, location: location, moveID: last.ID, at: last.At.UTC(), holder: *h}, nil
}
+
+// movesParkLock mirrors the park engine's lock predicate (park's isLock over
+// locationOf, both unexported): a key is a lock exactly when it is the
+// lock.json at the root of its own park location, instances/<host>,
+// instances/<host>/<incarnation> or moves/<host>. That is the writer's
+// lease at a holder location (lease.Acquire) and a park's own lease.
+// Decide never makes it a candidate, so the close need not vouch for it.
+// TestMovesParkLockMatchesTheEngine keeps the two in step through
+// park.Decide.
+func movesParkLock(host, key string) bool {
+ loc, ok := movesInstancesLocation(host, key)
+ if !ok {
+ if !strings.HasPrefix(key, "moves/"+host+"/") {
+ return false
+ }
+ loc = "moves/" + host
+ }
+ return key == loc+"/lock.json"
+}
diff --git a/internal/host/movesevidence_test.go b/internal/host/movesevidence_test.go
index 67a9e87e4..b6c3153b0 100644
--- a/internal/host/movesevidence_test.go
+++ b/internal/host/movesevidence_test.go
@@ -828,3 +828,70 @@ func TestAuthoritativeSweepStampsTheCloseOnlyOnWhatTheMoveRetired(t *testing.T)
}
})
}
+
+// TestMovesParkLockMatchesTheEngine (fix round 2): the provider mirrors the
+// park engine's unexported lock predicate (park's isLock over locationOf),
+// so it skips exactly the lease objects Decide skips. park.Decide is the
+// oracle: over a one-key census in a non-authoritative region, where every
+// non-lock object is a candidate, it decides nothing exactly for a lock.
+func TestMovesParkLockMatchesTheEngine(t *testing.T) {
+ f := newEvFixture(t)
+ obs := f.observe(t)
+ view := park.View{ClaimETag: obs.ETag, Region: "ie", AuthoritativeRegion: "uk",
+ GenerationOf: func(r park.Ref) (string, bool) { return landingGenerationOf(f.location, r.Key) }}
+ other := strings.Repeat("ab", 32)
+ for _, key := range []string{
+ "instances/" + evHost + "/lock.json",
+ "instances/" + evHost + "/" + f.inc + "/lock.json",
+ "instances/" + evHost + "/" + other + "/lock.json",
+ "moves/" + evHost + "/lock.json",
+ "moves/" + evHost + "/" + evMoveID + "/lock.json",
+ "instances/" + evHost + "/g-old/lock.json",
+ "instances/" + evHost + "/" + f.inc + "/g-old/lock.json",
+ "instances/" + evHost + "/lock.json.old",
+ "instances/" + evHost + "/0000/db",
+ } {
+ d, err := park.Decide(obs, view, []lease.ObjectVersion{{Key: key, VersionID: "v1", LastModified: evAt.Add(-time.Hour)}}, f.inc)
+ if err != nil {
+ t.Fatalf("%s: %v", key, err)
+ }
+ if engine, mirror := len(d.Candidates) == 0, movesParkLock(evHost, key); engine != mirror {
+ t.Errorf("%s: the engine skips it %v, the provider's mirror says lock %v", key, engine, mirror)
+ }
+ }
+}
+
+// TestLiveLocationLeaseIsSweptNormally (fix round 2): the writer's lease,
+// <holder.location>/lock.json (lease.Acquire), stands at every live
+// location. It is the engine's to skip, never residue the close must
+// vouch for, so the sweep proceeds, scoped and bare alike.
+func TestLiveLocationLeaseIsSweptNormally(t *testing.T) {
+ for name, bare := range map[string]bool{"scoped": false, "bare": true} {
+ t.Run(name, func(t *testing.T) {
+ f := newEvFixture(t)
+ if bare {
+ f.bare(t)
+ }
+ f.lane(evMoveID, "move.json")
+ f.bucket.Put("data", f.genKey(evMoveID), []byte("live"))
+ f.bucket.Put("data", f.location+"/generation.json", []byte("marker"))
+ f.bucket.Put("data", f.location+"/lock.json", []byte("writer lease"))
+ if _, _, err := f.provider().VerifyRetirement(t.Context(), f.observe(t), f.census(t)); err != nil {
+ t.Fatalf("VerifyRetirement refused the writer's lease: %v", err)
+ }
+ listing := evAt.Add(time.Minute)
+ w := f.worker(t, listing)
+ f.authoritative(t, &w, evMoveID)
+ if err := w.Run(t.Context()); !errors.Is(err, park.ErrPending) {
+ t.Fatalf("the lane chore waits for its horizon: %v", err)
+ }
+ chores := f.chores(t)
+ if moves, ok := chores["moves/"+evHost]; !ok || len(chores) != 1 || !moves.EvidenceAt.Equal(listing) {
+ t.Fatalf("chores = %+v; want the moves chore alone", chores)
+ }
+ if vs, err := f.bucket.ListVersions(t.Context(), "data", f.location+"/lock.json"); err != nil || len(vs) != 1 {
+ t.Fatalf("the lease was touched: %v, %v", vs, err)
+ }
+ })
+ }
+}