carlos / platform Public

moves: run the source and rollback-return cleanups as real parks (Task 9a fix round 1, I3)

The two destructive-step tests that only inspected the view now each have a
companion that runs the real view and park engine through the step:

- TestSourceCleanupKeepsTheFilesLiveSetUntilTheCommit (the source
  cleanup): with the target's probe admitted and the record not committed,
  a park of the source's location in its own region, the scope the commit
  queues, is refused and the source's files stay byte for byte; after the
  commit the source's finalization runs its location park through the real
  view and the files are retired, archived under parked/.
- TestRollbackReturnCleanupKeepsTheFilesLiveSet (the rollback return's
  cleanup): the target region's location park the release queues is
  refused while the target's admitted generation is the holder's; once the
  return names the source's generation and the source recovers, the
  target's cleaning runs it and the files are retired.

RED (the J2 source rule and the restoring admission rule removed in place):
both parks ran before their boundary ("waiting for verification after
horizon"). The view-only tests stay, for the view's own answers.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 630f9c5a424fd51b97935bf05cf07ffbecf3d131 parent 853ce03
1 file changed, +156 −0
  • internal/host/movesfileslive_test.go +156 −0
diff --git a/internal/host/movesfileslive_test.go b/internal/host/movesfileslive_test.go
index 32e22729a..9ed75f05e 100644
--- a/internal/host/movesfileslive_test.go
+++ b/internal/host/movesfileslive_test.go
@@ -294,3 +294,159 @@ func TestFilesLiveSetFollowsTheLiveChain(t *testing.T) {
t.Fatalf("g2's object did not survive the park: %q %v", got, ok)
}
}
+
+// putFilesGeneration writes, in bucket, a files object of gen at location
+// and gen's manifest naming it, returning the object's key.
+func putFilesGeneration(t *testing.T, vs *fakeVersioned, bucket, location, gen string) string {
+ t.Helper()
+ ctx := context.Background()
+ obj := location + "/files/g-" + gen + "/objects/0000000000000001-aa"
+ if _, err := vs.PutIfAbsent(ctx, bucket, obj, []byte("a live file")); err != nil {
+ t.Fatal(err)
+ }
+ filesManifest(t, func(k string, b []byte) {
+ if _, err := vs.PutIfAbsent(ctx, bucket, k, b); err != nil {
+ t.Fatal(err)
+ }
+ }, location, gen, "a.txt="+obj)
+ return obj
+}
+
+// archivedUnder reports whether bucket holds a parked copy of key, a key
+// under location (park's destination: parked/<inc>/<host>/<retirement>/
+// instances/<key below the location>@<version>).
+func archivedUnder(f *fakeMoveMirror, bucket, inc, location, key string) bool {
+ tail := strings.TrimPrefix(key, location+"/")
+ for k := range f.buckets[bucket] {
+ if strings.HasPrefix(k, "parked/"+inc+"/") && strings.Contains(k, "/instances/"+tail+"@") {
+ return true
+ }
+ }
+ return false
+}
+
+// admitTarget is the target's probe admission on the claim (as Plan 3's
+// probe makes it): holder.generation is the move's, admitted in uk.
+func admitTarget(t *testing.T, h *landingHarness) {
+ t.Helper()
+ rewriteP1Claim(t, h.s, func(c *hostclaim.Claim) {
+ c.Holder.Admissions = append(c.Holder.Admissions, hostclaim.AdmissionRecord{Region: "uk", PredecessorRegion: "ie", Session: c.Holder.Session + 1, Box: "uk-box", Generation: p1ID, Predecessor: c.Holder.Generation, Mode: "cold"})
+ c.Holder.Generation = p1ID
+ })
+}
+
+// TestSourceCleanupKeepsTheFilesLiveSetUntilTheCommit (fix round 1, I3; the
+// source cleanup, J2/F4): the source's location park runs through the real
+// view and engine. Before the record commits, with the target's probe
+// admitted, a park of the source's location in its own region is refused
+// and the source's files stay byte for byte. After the commit the source's
+// finalization runs that park and the files are retired, archived under
+// parked/.
+func TestSourceCleanupKeepsTheFilesLiveSetUntilTheCommit(t *testing.T) {
+ ctx := context.Background()
+ h := newFinalHarness(t)
+ h.advanceToProbed(t)
+ admitTarget(t, h.landingHarness)
+ location := strings.TrimSuffix(h.claim(t).Holder.Location, "/")
+ obj := putFilesGeneration(t, h.vs, "data-ie", location, "cur")
+ view := movesParkView(MovesRecordsFrom(h.s), h.vs, "data-ie", "ie")
+
+ // Before the commit: the same scope the commit queues, run by the engine.
+ rec := h.read(t, p1Host)
+ scope, err := sourceLocationParkScope(rec, *h.claim(t).Holder)
+ if err != nil {
+ t.Fatal(err)
+ }
+ early := hostclaim.Obligation{ID: obligationID(), Op: p1ID, Kind: "park-replica", Region: "ie", Owner: "region:ie", Retirement: obligationID(), Park: &scope}
+ rewriteP1Claim(t, h.s, func(c *hostclaim.Claim) { c.Obligations = append(c.Obligations, early) })
+ e := park.Engine{Store: h.vs, Claims: h.s, Bucket: "data-ie", Region: "ie", Leases: h.leases, Now: h.clock.Now, NewID: obligationID,
+ View: view, Authorize: func(context.Context, hostclaim.Observed, hostclaim.Obligation) error { return nil }}
+ if err := e.Run(ctx, park.Work{Host: p1Host, ID: early.ID, Op: p1ID}); !errors.Is(err, park.ErrUnsafe) {
+ t.Fatalf("a park of the source before the commit: %v", err)
+ }
+ if body, ok := h.f.buckets["data-ie"][obj]; !ok || body.body != "a live file" {
+ t.Fatalf("the source's file before the commit: %+v %v", body, ok)
+ }
+ rewriteP1Claim(t, h.s, func(c *hostclaim.Claim) {
+ kept := c.Obligations[:0]
+ for _, o := range c.Obligations {
+ if o.ID != early.ID {
+ kept = append(kept, o)
+ }
+ }
+ c.Obligations = kept
+ })
+
+ // The commit, then the source's finalization through the real view.
+ h.commitRecord(t)
+ h.commitClaim(t)
+ h.cfg.ParkView = view
+ h.sourcePass(t)
+ if m := h.read(t, p1Host).Move; m.SourceParkedAt == nil {
+ t.Fatalf("the source did not finalize: %+v", m)
+ }
+ if _, ok := h.f.buckets["data-ie"][obj]; ok {
+ t.Fatal("the source's file was not retired after the commit")
+ }
+ if !archivedUnder(h.f, "data-ie", h.inc, location, obj) {
+ t.Fatal("the source's file was not archived")
+ }
+}
+
+// TestRollbackReturnCleanupKeepsTheFilesLiveSet (fix round 1, I3; the
+// rollback return's cleanup): the target's region location park, queued
+// when the target released, runs through the real view and engine. While
+// the target's admitted generation is still the holder's (before the return
+// names the source's again) the target region is authoritative and the park
+// is refused, the target's files kept; once the source has taken the host
+// back, the target's cleaning runs it and the files are retired.
+func TestRollbackReturnCleanupKeepsTheFilesLiveSet(t *testing.T) {
+ ctx := context.Background()
+ h := newRollbackHarness(t)
+ admitTarget(t, h.landingHarness)
+ location := strings.TrimSuffix(h.claim(t).Holder.Location, "/")
+ body, err := json.Marshal(map[string]any{"op": p1ID, "incarnation": h.inc, "host": p1Host, "box": "uk-box", "installed_at": h.clock.Now(), "manifest_sha256": "x", "snapshot_txid": 1})
+ if err != nil {
+ t.Fatal(err)
+ }
+ if _, err := h.vs.PutIfAbsent(ctx, "data-uk", location+"/generation.json", body); err != nil {
+ t.Fatal(err)
+ }
+ obj := putFilesGeneration(t, h.vs, "data-uk", location, p1ID)
+ view := movesParkView(MovesRecordsFrom(h.s), h.vs, "data-uk", "uk")
+ h.parkView = view
+ h.rollBack(t)
+ h.rollbackPass(t)
+ var chore *hostclaim.Obligation
+ for _, o := range h.claim(t).Obligations {
+ if o.Op == p1ID && o.Kind == "park-replica" && o.Region == "uk" && o.Park != nil && o.Park.Location == location {
+ chore = &o
+ }
+ }
+ if chore == nil {
+ t.Fatalf("the release queued no target location park: %+v", h.claim(t).Obligations)
+ }
+ e := park.Engine{Store: h.vs, Claims: h.s, Bucket: "data-uk", Region: "uk", Leases: h.leases, Now: h.clock.Now, NewID: obligationID,
+ View: view, Authorize: func(context.Context, hostclaim.Observed, hostclaim.Obligation) error { return nil }}
+ if err := e.Run(ctx, park.Work{Host: p1Host, ID: chore.ID, Op: p1ID}); !errors.Is(err, park.ErrUnsafe) {
+ t.Fatalf("a park of the target while its generation is live: %v", err)
+ }
+ if b, ok := h.f.buckets["data-uk"][obj]; !ok || b.body != "a live file" {
+ t.Fatal("the target's file was taken before the return")
+ }
+
+ // The source takes the host back: the return names its generation again
+ // and it recovers; the target's cleaning then runs the park.
+ rewriteP1Claim(t, h.s, func(c *hostclaim.Claim) { c.Holder.Generation = "cur" })
+ h.recovered(t)
+ h.rollbackPass(t)
+ if rb := h.rb(t); rb.TargetPhase != provision.RbTargetCleaned {
+ t.Fatalf("the target did not clean: %+v", rb)
+ }
+ if _, ok := h.f.buckets["data-uk"][obj]; ok {
+ t.Fatal("the target's file was not retired by the cleaning")
+ }
+ if !archivedUnder(h.f, "data-uk", h.inc, location, obj) {
+ t.Fatal("the target's file was not archived")
+ }
+}