moves: remove the park stand-ins; the landing holds its lease from checking (Plan 5 Task 9a, step 2)
Nothing stands in for the claim's park view or authorizer in production: - the landing and dirty-return installer (moveslandpark.go), the rollback target (movesrollback.go), the lane parks of the revert, abort and committed landing (movesrevert.go) and the source finalization (movesfinal.go) take ParkView as it is and run the claim's ParkAuthorize behind their own operation check (movesArmAuthorize, M17); - every arm requires ParkLeases, ParkView and ParkAuthorize (parkHooksArmed); the landing and the recovery also claim Plan 12's RunHeld, the finalization its local authorizer (M9, no stand-in). M9's named exceptions: the install's and the rollback's park.Local authorizers keep their own rule while ParkLocalAuthorize is unset, each marked "claim Plan 12 F". M5: the landing takes its own-location lease at checking (spec §7.6) and holds it through published; the installer's parking takes it too and runs each residue chore through ParkRunHeld under the held lease, since the residue scope's lease set is that same lock. Tests: standInView moves to the tests as a fake (movesparkfakes_test.go, armParkFakes); TestNoArmRunsWithoutTheThreeParkHooks, TestLandingResidueParkUnderItsOwnLease (real AggregateLeases and the moves lease set); checking now takes exactly the location's lease.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
15 files changed,
+425
−159
internal/host/movesabort_test.go+1 −1internal/host/movesfinal.go+37 −17internal/host/movesfinal_test.go+11 −3internal/host/movesinstall.go+35 −6internal/host/movesinstall_test.go+1 −0internal/host/movesland.go+18 −3internal/host/movesland_test.go+29 −15internal/host/moveslandpark.go+64 −64internal/host/movesparkfakes_test.go+76 −0internal/host/movesrecover.go+3 −2internal/host/movesrecover_test.go+1 −0internal/host/movesrevert.go+18 −27internal/host/movesrevert_test.go+1 −1internal/host/movesrollback.go+27 −20internal/host/movessources_test.go+103 −0
diff --git a/internal/host/movesabort_test.go b/internal/host/movesabort_test.go| index 44b728d79..b725d62c0 100644 |
| --- a/internal/host/movesabort_test.go |
| +++ b/internal/host/movesabort_test.go |
| @@ -67,7 +67,7 @@ func (h *abortHarness) abort(t *testing.T) { |
| func (h *abortHarness) laneCfg() moveCfg { |
| c := h.targetCfg() |
| c.DataDir = h.tdir |
| - c.ParkLeases = h.tleases |
| + armParkFakes(&c, h.s, h.tleases, c.Region, roleTarget) |
| return c |
| } |
diff --git a/internal/host/movesfinal.go b/internal/host/movesfinal.go| index 3c3ed9d32..01ce52ef4 100644 |
| --- a/internal/host/movesfinal.go |
| +++ b/internal/host/movesfinal.go |
| @@ -69,11 +69,11 @@ import ( |
| // still has the row or the op's journal. Plan 4's rollback visits reuse it. |
| // |
| // Arming. Every hook that exists on main is wired (the guard, park.Local, the |
| -// registry's delete, the versioned store), but the park engine's leases and |
| -// Plan 5's view and authorizer are not (ruling G17, as for the landing), so |
| -// sourceFinalArmed answers false in production and the pass only logs. The |
| -// tests arm it with fakes and the stand-ins below. No production record |
| -// reaches committed before Task 7 and Group D anyway. |
| +// registry's delete, the versioned store). The claim's park leases, view and |
| +// authorizer come from its MoveParks (armMovesParks, Plan 5 Task 9a), and |
| +// park.Local's authorizer is claim Plan 12's (M9); armMovesParks sets them |
| +// only with claim Plan 12, so sourceFinalArmed answers false in production |
| +// and the pass only logs until it lands. The tests arm it with fakes. |
| // The finalization's op journal steps (moves.Journal, the source's own |
| // journal of this op, which the cut began). |
| @@ -167,13 +167,13 @@ func (w *movesWorker) participantRound(ctx context.Context, s release.Store, rec |
| } |
| // sourceFinalArmed reports whether this box can run the finalization: every |
| -// hook it calls is wired, the park engine's leases and Plan 5's view and |
| -// authorizer included. Nothing stands in for a missing owner in production |
| -// (fix round 1): the tests inject their own. In production none of the |
| -// three is wired yet (ruling G17). |
| +// hook it calls is wired, the claim's three park hooks and claim Plan 12's |
| +// local authorizer (M9) included. Nothing stands in for a missing owner: |
| +// the tests inject their own. In production armMovesParks sets them only |
| +// with claim Plan 12, so this answers false there until it lands. |
| func sourceFinalArmed(cfg moveCfg) bool { |
| return cfg.Box != "" && cfg.DataDir != "" && cfg.Guard.DataDir != "" && cfg.Local.DataDir != "" && |
| - cfg.ParkLeases != nil && cfg.ParkView != nil && cfg.ParkAuthorize != nil && |
| + parkHooksArmed(cfg) && cfg.ParkLocalAuthorize != nil && |
| cfg.Versioned != nil && cfg.Route != nil && cfg.DeleteHeld != nil |
| } |
| @@ -376,12 +376,24 @@ func (f *sourceFinal) sourceOf(obs hostclaim.Observed) error { |
| return nil |
| } |
| -// engine is the park engine over this box's own bucket, with the wired |
| -// leases, view and authorizer (sourceFinalArmed requires all three). |
| +// engine is the park engine over this box's own bucket, with the claim's |
| +// leases and view, and the claim's authorizer behind the finalization's own |
| +// check (sourceFinalArmed requires all three): the claim is still this |
| +// move's commit with this box its source, and the chore is this op's in |
| +// this region. |
| func (f *sourceFinal) engine(vs lease.VersionedStore) park.Engine { |
| cfg := f.cfg() |
| + own := func(_ context.Context, obs hostclaim.Observed, o hostclaim.Obligation) error { |
| + if err := f.sourceOf(obs); err != nil { |
| + return err |
| + } |
| + if o.Op != f.g.ID || o.Region != cfg.Region { |
| + return fmt.Errorf("%w: not this finalization's chore", park.ErrUnsafe) |
| + } |
| + return nil |
| + } |
| return park.Engine{Store: vs, Claims: f.s, Bucket: cfg.OwnBucket, Region: cfg.Region, Leases: cfg.ParkLeases, |
| - Now: f.now, NewID: obligationID, View: cfg.ParkView, Authorize: cfg.ParkAuthorize, After: cfg.parkFault} |
| + Now: f.now, NewID: obligationID, View: cfg.ParkView, Authorize: movesArmAuthorize(cfg.ParkAuthorize, own), After: cfg.parkFault} |
| } |
| // remote is step 1. done is true once the source region's location park has |
| @@ -479,11 +491,19 @@ func (f *sourceFinal) installation(ctx context.Context) (string, error) { |
| local.Box = cfg.Box |
| } |
| if local.Authorize == nil { |
| - // park.Local asks the same owner the engine does, over the claim |
| - // it read under the guard. |
| - authorize := cfg.ParkAuthorize |
| + // park.Local asks claim Plan 12's local authorizer (M9), behind |
| + // the finalization's own check over the claim it read under the |
| + // guard. The claim's engine authorizer refuses a park-local, and |
| + // nothing stands in: sourceFinalArmed requires it. |
| + authorize := cfg.ParkLocalAuthorize |
| local.Authorize = func(ctx context.Context, c *hostclaim.Claim, o hostclaim.Obligation) error { |
| - return authorize(ctx, hostclaim.Observed{Host: host, Claim: c}, o) |
| + if err := f.sourceOf(hostclaim.Observed{Host: host, Claim: c}); err != nil { |
| + return err |
| + } |
| + if o.Op != id || o.Kind != "park-local" || o.Box != cfg.Box { |
| + return fmt.Errorf("%w: not this finalization's local chore", park.ErrUnsafe) |
| + } |
| + return authorize(ctx, c, o) |
| } |
| } |
| if err := local.RetireInstallation(ctx, host, hostguard.Captured{Incarnation: captured.Incarnation, Stamp: stamp}, chore.ID, id); err != nil { |
diff --git a/internal/host/movesfinal_test.go b/internal/host/movesfinal_test.go| index 236dc2510..c4cf9d286 100644 |
| --- a/internal/host/movesfinal_test.go |
| +++ b/internal/host/movesfinal_test.go |
| @@ -94,6 +94,7 @@ func newFinalHarness(t *testing.T) *finalHarness { |
| h.cfg.Local = park.Local{DataDir: dd, Box: "ie-box"} |
| h.cfg.ParkLeases = h.leases |
| h.cfg.ParkView, h.cfg.ParkAuthorize = sourceTestView, sourceTestAuthorize |
| + h.cfg.ParkLocalAuthorize = sourceTestLocalAuthorize |
| h.cfg.parkFault = h.lcfg.parkFault |
| h.cfg.finalFault = func(point string) error { |
| h.finalHits = append(h.finalHits, point) |
| @@ -751,9 +752,10 @@ func TestSourceParkWaitsUnarmedInProduction(t *testing.T) { |
| h := newFinalHarness(t) |
| h.commit(t) |
| for name, unarm := range map[string]func(*moveCfg){ |
| - "no park leases": func(c *moveCfg) { c.ParkLeases = nil }, |
| - "no park view": func(c *moveCfg) { c.ParkView = nil }, |
| - "no park authorizer": func(c *moveCfg) { c.ParkAuthorize = nil }, |
| + "no park leases": func(c *moveCfg) { c.ParkLeases = nil }, |
| + "no park view": func(c *moveCfg) { c.ParkView = nil }, |
| + "no park authorizer": func(c *moveCfg) { c.ParkAuthorize = nil }, |
| + "no claim Plan 12 local authorizer": func(c *moveCfg) { c.ParkLocalAuthorize = nil }, |
| } { |
| saved := h.cfg |
| unarm(&h.cfg) |
| @@ -866,6 +868,12 @@ func sourceTestView(_ context.Context, obs hostclaim.Observed) (park.View, error |
| GenerationOf: func(r park.Ref) (string, bool) { return landingGenerationOf(location, r.Key) }}, nil |
| } |
| +// sourceTestLocalAuthorize is claim Plan 12's local authorizer (M9) for the |
| +// source's park-local: sourceTestAuthorize over the claim park.Local read. |
| +func sourceTestLocalAuthorize(ctx context.Context, c *hostclaim.Claim, o hostclaim.Obligation) error { |
| + return sourceTestAuthorize(ctx, hostclaim.Observed{Host: p1Host, Claim: c}, o) |
| +} |
| + |
| func sourceTestAuthorize(_ context.Context, obs hostclaim.Observed, o hostclaim.Obligation) error { |
| c := obs.Claim |
| if obs.Absent || c == nil || c.Op.ID != p1ID || c.Op.Kind != hostclaim.KindLanding || c.Op.Phase != hostclaim.PhaseCommitted || |
diff --git a/internal/host/movesinstall.go b/internal/host/movesinstall.go| index f90509e38..8c7ea8ebd 100644 |
| --- a/internal/host/movesinstall.go |
| +++ b/internal/host/movesinstall.go |
| @@ -215,7 +215,22 @@ func newInstallRun(s release.Store, w *movesWorker, rec provision.Record, src in |
| ir.local.Box = cfg.Box |
| } |
| if ir.local.Authorize == nil { |
| - ir.local.Authorize = ir.authorizeChore |
| + // park.Local's authorizer: claim Plan 12's local authorizer |
| + // (ParkLocalAuthorize) behind this install's own check; the claim's |
| + // engine authorizer refuses a park-local (M9). Until claim Plan 12 |
| + // lands this install's own rule stands alone: a named exception, the |
| + // first of the two. claim Plan 12 F: drop the fallback once |
| + // ParkLocalAuthorize is always wired. |
| + plan12 := cfg.ParkLocalAuthorize |
| + ir.local.Authorize = func(ctx context.Context, c *hostclaim.Claim, o hostclaim.Obligation) error { |
| + if err := ir.authorizeChore(ctx, c, o); err != nil { |
| + return err |
| + } |
| + if plan12 != nil { |
| + return plan12(ctx, c, o) |
| + } |
| + return nil |
| + } |
| } |
| return ir |
| } |
| @@ -347,10 +362,12 @@ func (ir *installRun) versioned(ctx context.Context) (lease.VersionedStore, erro |
| // @root), scoped to the location with the flip's time as the evidence |
| // horizon. On the target that is an earlier stay's residue; on the source |
| // of a dirty return it is the pre-move generations (§7.8, D6). Each chore is |
| -// run once per pass through the park engine, which takes the location's |
| -// lease itself (ErrPending is the expected answer: the acknowledgement waits |
| -// for the horizon, and is C4's runner's); the phase advances once every |
| -// chore's manifest exists and its deletion set is gone (Engine.Parked). |
| +// run once per pass through the park engine. The location's lease is the |
| +// install's own, held from here (a landing's from checking) through |
| +// published, and each chore runs under it through claim Plan 12's RunHeld |
| +// (M5). ErrPending is the expected answer: the acknowledgement waits for |
| +// the horizon, and is C4's runner's. The phase advances once every chore's |
| +// manifest exists and its deletion set is gone (Engine.Parked). |
| // Per-generation chores are what let the acknowledgement finish after the |
| // probe makes the new generation live here. |
| // |
| @@ -424,6 +441,15 @@ func (ir *installRun) parking(ctx context.Context) (bool, error) { |
| if ir.rec.Move.FlippedAt == nil { |
| return false, fmt.Errorf("move %s: the record has no flip time", id) |
| } |
| + // The parks run under the build's own lease on the location, which the |
| + // install holds from here (a landing from checking) through published: |
| + // the residue scope's lease set is that lock (M6), so Engine.Run would |
| + // wait on the install itself. claim Plan 12's RunHeld runs each chore |
| + // under the held lease instead (M5). |
| + held, waiting, err := ir.lease(ctx, holder) |
| + if waiting || err != nil { |
| + return false, err |
| + } |
| scope := moves.ResidueScope(holder, *ir.rec.Move.FlippedAt) |
| if obs, err = hostclaim.Update(ctx, ir.s, obs, hostclaim.QueueResidueParks(id, cfg.Region, scope, gens, obligationID)); err != nil { |
| return false, fmt.Errorf("queue the residue parks: %w", err) |
| @@ -438,7 +464,10 @@ func (ir *installRun) parking(ctx context.Context) (bool, error) { |
| continue |
| } |
| w := park.Work{Host: host, ID: o.ID, Op: id} |
| - if err := eng.Run(ctx, w); err != nil && !errors.Is(err, park.ErrPending) { |
| + if cfg.ParkRunHeld == nil { |
| + return false, fmt.Errorf("move %s: %w", id, &ClaimDependencyError{Contracts: movesPlan12Contracts[:1]}) |
| + } |
| + if err := cfg.ParkRunHeld(ctx, eng, w, held); err != nil && !errors.Is(err, park.ErrPending) { |
| return false, fmt.Errorf("park generation %s: %w", o.Generation, err) |
| } |
| parked, err := eng.Parked(ctx, w) |
diff --git a/internal/host/movesinstall_test.go b/internal/host/movesinstall_test.go| index 631dc7fc1..18689869b 100644 |
| --- a/internal/host/movesinstall_test.go |
| +++ b/internal/host/movesinstall_test.go |
| @@ -187,6 +187,7 @@ func (h *returnHarness) sourceCfg(t *testing.T) moveCfg { |
| } |
| c.Guard = hostguard.Guard{DataDir: h.sdir, LockDir: t.TempDir()} |
| c.Local = park.Local{DataDir: h.sdir, Box: "ie-box"} |
| + c.ParkView = recordView(h.s, "ie", roleSource) |
| c.RenderHeld = func(_ context.Context, _ release.Store, rt registry.Route, op string) (appconfig.HeldRender, error) { |
| h.renders++ |
| if op != p1RB || rt.HoldOp != p1RB { |
diff --git a/internal/host/movesland.go b/internal/host/movesland.go| index 6367404db..90e97c770 100644 |
| --- a/internal/host/movesland.go |
| +++ b/internal/host/movesland.go |
| @@ -128,11 +128,20 @@ type landingRun struct { |
| guard hostguard.Guard |
| } |
| +// parkHooksArmed reports whether the claim's three park hooks are wired: |
| +// the leases, the view and the authorizer (M17). Every moves arm requires |
| +// all three; nothing stands in for a missing one. |
| +func parkHooksArmed(cfg moveCfg) bool { |
| + return cfg.ParkLeases != nil && cfg.ParkView != nil && cfg.ParkAuthorize != nil |
| +} |
| + |
| // landingArmed reports whether this box can run the landing: every hook |
| -// the phases call is wired. In production ParkLeases, ParkView and |
| -// ParkAuthorize are not (ruling G17), so this answers false there. |
| +// the phases call is wired, the claim's three park hooks and claim Plan 12's |
| +// RunHeld (M5: the residue parks run under the landing's own lease) among |
| +// them. In production armMovesParks sets those only with claim Plan 12, so |
| +// this answers false there until it lands. |
| func landingArmed(cfg moveCfg) bool { |
| - return cfg.Box != "" && cfg.Guard.DataDir != "" && cfg.Local.DataDir != "" && cfg.ParkLeases != nil && |
| + return cfg.Box != "" && cfg.Guard.DataDir != "" && cfg.Local.DataDir != "" && parkHooksArmed(cfg) && cfg.ParkRunHeld != nil && |
| cfg.OwnLease != nil && cfg.RenderHeld != nil && cfg.ReplaceTimers != nil && cfg.SetReplication != nil && |
| cfg.SetNextDue != nil && cfg.RemoveVerifyEntry != nil && cfg.WriteGen != nil && cfg.Route != nil && |
| cfg.FilesDir != nil && cfg.Versioned != nil && cfg.ReleaseReady != nil && cfg.SecretsPriv != nil |
| @@ -590,6 +599,12 @@ func (l *landingRun) checking(ctx context.Context) (bool, error) { |
| return false, fmt.Errorf("queue the local park: %w", err) |
| } |
| } |
| + // Spec §7.6 checking: the lease on the location in this box's own |
| + // bucket, held through published (M5). The residue parks run under it |
| + // (ParkRunHeld), so the landing never waits on its own lock. |
| + if _, waiting, err := acquireOwnLease(ctx, l.w, host, holder); waiting || err != nil { |
| + return false, err |
| + } |
| return l.advance(ctx, provision.LandChecking, provision.LandParking, nil) |
| } |
diff --git a/internal/host/movesland_test.go b/internal/host/movesland_test.go| index e4c39e4ab..54f0de4e2 100644 |
| --- a/internal/host/movesland_test.go |
| +++ b/internal/host/movesland_test.go |
| @@ -46,8 +46,10 @@ import ( |
| // |
| // fakeParkLeases is one lease table for the landing's own-location lease |
| // and the park engine's: a second acquisition of a held location is |
| -// refused, so a lease the landing still held while a residue park ran |
| -// would fail the test (round-2 finding 9). |
| +// refused. The landing holds its lease from checking through published |
| +// (spec §7.6), so its residue parks run under it through claim Plan 12's |
| +// RunHeld (testRunHeld, M5); an Engine.Run that took the lock again would |
| +// fail the test. |
| type landingHarness struct { |
| *cutHarness |
| @@ -92,8 +94,13 @@ type landingHarness struct { |
| releaseErr error |
| // ownAuthorize drops the park authorizer the target would inherit from |
| // the source's configuration (the finalization harness sets one), so |
| - // the target's parks use their own stand-ins (Plan 5 Task 3). |
| + // the target's parks use the allowing fake behind their own checks |
| + // (Plan 5 Task 3). |
| ownAuthorize bool |
| + // parkLeases and runHeld, when set, replace the park fakes' leases and |
| + // claim Plan 12's RunHeld (Plan 5 Task 9a, M5). |
| + parkLeases park.Leases |
| + runHeld func(ctx context.Context, e park.Engine, w park.Work, held park.HeldLease) error |
| } |
| // fakeParkLeases refuses a second acquisition of a held location. |
| @@ -340,18 +347,19 @@ func (h *landingHarness) landingCfg(t *testing.T) moveCfg { |
| } |
| c.Guard = hostguard.Guard{DataDir: h.tdir, LockDir: t.TempDir()} |
| c.Local = park.Local{DataDir: h.tdir, Box: "uk-box"} |
| - c.ParkLeases = h.leases |
| - c.ParkView = func(ctx context.Context, obs hostclaim.Observed) (park.View, error) { |
| - if h.parkView == nil { |
| - return park.View{}, errors.New("no view override") |
| - } |
| - return h.parkView(ctx, obs) |
| + inherited := c.ParkAuthorize |
| + armParkFakes(&c, h.s, h.leases, c.Region, roleTarget) |
| + if h.parkView != nil { |
| + c.ParkView = func(ctx context.Context, obs hostclaim.Observed) (park.View, error) { return h.parkView(ctx, obs) } |
| + } |
| + if inherited != nil && !h.ownAuthorize { |
| + c.ParkAuthorize = inherited |
| } |
| - if h.parkView == nil { |
| - c.ParkView = nil |
| + if h.parkLeases != nil { |
| + c.ParkLeases = h.parkLeases |
| } |
| - if h.ownAuthorize { |
| - c.ParkAuthorize = nil |
| + if h.runHeld != nil { |
| + c.ParkRunHeld = h.runHeld |
| } |
| c.OwnLease = h.leases.Acquire |
| if h.store != nil { |
| @@ -719,8 +727,14 @@ func TestLandingCheckingRequiresClaimEvidenceAndTouch(t *testing.T) { |
| if err != nil || captured.Stamp != c.Holder.Stamp || captured.Incarnation != h.inc { |
| t.Fatalf("active_stamp %+v %v, want F %d", captured, err, c.Holder.Stamp) |
| } |
| - if len(h.leases.acquired) != 0 { |
| - t.Fatalf("checking took a lease: %v", h.leases.acquired) |
| + // Spec §7.6 checking (Plan 5 Task 9a, M5): the location's lease in |
| + // the target's own bucket, taken once and held into parking. |
| + want := "data-uk/" + strings.TrimSuffix(c.Holder.Location, "/") |
| + if len(h.leases.acquired) != 1 || h.leases.acquired[0] != want { |
| + t.Fatalf("checking took %v, want the location's lease %s", h.leases.acquired, want) |
| + } |
| + if held := h.leases.holding(); len(held) != 1 || held[0] != want { |
| + t.Fatalf("after checking the landing holds %v", held) |
| } |
| }) |
| t.Run("a nested row is refused for a rollback", func(t *testing.T) { |
diff --git a/internal/host/moveslandpark.go b/internal/host/moveslandpark.go| index 2c08a40b0..76244f0b9 100644 |
| --- a/internal/host/moveslandpark.go |
| +++ b/internal/host/moveslandpark.go |
| @@ -15,17 +15,39 @@ import ( |
| "github.com/carlosframework/platform/internal/provision" |
| ) |
| -// The installer's park and lease adapters (moves spec §6.4, §7.6; ruling |
| -// G17), shared by the landing and the dirty return: the park engine over |
| -// this box's own bucket for the residue parks, the stand-in view and |
| -// authorizer the tests use until Plan 5's providers, the generation |
| -// classifier they share, and the own-location lease the build holds. The |
| -// phases are in movesinstall.go, the landing's pass in movesland.go. |
| +// The installer's park and lease adapters (moves spec §6.4, §7.6), shared by |
| +// the landing and the dirty return: the park engine over this box's own |
| +// bucket for the residue parks, with the claim's view and authorizer |
| +// (MoveParks, armMovesParks), the generation classifier the view shares, and |
| +// the own-location lease the build holds from checking through published. |
| +// The phases are in movesinstall.go, the landing's pass in movesland.go. |
| +// |
| +// Nothing stands in for the claim's view or authorizer (Plan 5 Task 9a): an |
| +// arm without ParkView and ParkAuthorize is not armed, and the tests inject |
| +// fakes. Each arm keeps its own operation check around the claim's |
| +// authorizer (M17, movesArmAuthorize). The one exception is park.Local's |
| +// authorizer for the install's own local chores (M9): until claim Plan 12's |
| +// local authorizer is wired as ParkLocalAuthorize, the install keeps its own |
| +// rule (installRun.authorizeChore). |
| -// authorizeMoveChore is park.Local.Authorize's and park.Engine.Authorize's |
| -// stand-in rule until Plan 5: a chore is run only when it is the op's own |
| -// and the claim still names the op, at the install's kind, with this box |
| -// executing. |
| +// movesArmAuthorize is an arm's park authorizer (M17): the arm's own |
| +// operation check, then the claim's authorizer, ParkAuthorize from the |
| +// claim's MoveParks. A missing claim authorizer refuses; nothing stands in. |
| +func movesArmAuthorize(claim, own func(context.Context, hostclaim.Observed, hostclaim.Obligation) error) func(context.Context, hostclaim.Observed, hostclaim.Obligation) error { |
| + return func(ctx context.Context, obs hostclaim.Observed, o hostclaim.Obligation) error { |
| + if claim == nil || own == nil { |
| + return fmt.Errorf("%w: no park authorizer", park.ErrUnsafe) |
| + } |
| + if err := own(ctx, obs, o); err != nil { |
| + return err |
| + } |
| + return claim(ctx, obs, o) |
| + } |
| +} |
| + |
| +// authorizeMoveChore is an install's own operation check: a chore is run |
| +// only when it is the op's own and the claim still names the op, at the |
| +// install's kind, with this box executing. |
| func authorizeMoveChore(c *hostclaim.Claim, o hostclaim.Obligation, opID, kind, box string) error { |
| if c == nil || o.Op != opID || c.Op.ID != opID || c.Op.Kind != kind || c.Op.Exec != box { |
| return fmt.Errorf("%w: not this install's chore", park.ErrUnsafe) |
| @@ -33,13 +55,17 @@ func authorizeMoveChore(c *hostclaim.Claim, o hostclaim.Obligation, opID, kind, |
| return nil |
| } |
| -// authorizeChore is park.Local.Authorize's stand-in for this install. |
| +// authorizeChore is park.Local.Authorize for this install's own local |
| +// chores while claim Plan 12's local authorizer is missing: a named |
| +// exception (M9), the only park stand-in left in production. |
| +// claim Plan 12 F: replace it with ParkLocalAuthorize once the MoveParks |
| +// capability carries the local authorizer. |
| func (ir *installRun) authorizeChore(_ context.Context, c *hostclaim.Claim, o hostclaim.Obligation) error { |
| return authorizeMoveChore(c, o, ir.g.ID, ir.src.kind(), ir.cfg().Box) |
| } |
| -// authorizeObligation is park.Engine.Authorize's stand-in (ruling G17): the |
| -// same rule over an observation. |
| +// authorizeObligation is the install's own operation check over an |
| +// observation, which the engine's authorizer runs before the claim's. |
| func (ir *installRun) authorizeObligation(_ context.Context, obs hostclaim.Observed, o hostclaim.Obligation) error { |
| if obs.Absent { |
| return park.ErrUnsafe |
| @@ -47,8 +73,8 @@ func (ir *installRun) authorizeObligation(_ context.Context, obs hostclaim.Obser |
| return authorizeMoveChore(obs.Claim, o, ir.g.ID, ir.src.kind(), ir.cfg().Box) |
| } |
| -// authorizeObligation is the landing's engine stand-in, the installer's rule |
| -// for a landing. |
| +// authorizeObligation is the landing's own operation check, the installer's |
| +// rule for a landing. |
| func (l *landingRun) authorizeObligation(_ context.Context, obs hostclaim.Observed, o hostclaim.Obligation) error { |
| if obs.Absent { |
| return park.ErrUnsafe |
| @@ -56,34 +82,6 @@ func (l *landingRun) authorizeObligation(_ context.Context, obs hostclaim.Observ |
| return authorizeMoveChore(obs.Claim, o, l.g.ID, hostclaim.KindLanding, l.cfg().Box) |
| } |
| -// standInView is park.Engine.View's stand-in (ruling G17): the location's |
| -// objects classified by key, seen from region, and the other side of the |
| -// move authoritative, which §5.1's table says it is before this side's |
| -// probe is admitted (every install phase). role names this side (ruling |
| -// J24): a landing (roleTarget) works while the source's region, From, is |
| -// authoritative; a dirty return (roleSource) while the target's, To, is. |
| -// Plan 5's provider replaces it; a test may set cfg.ParkView to the view a |
| -// later state has. |
| -func standInView(region, role string, m *provision.MoveState) func(context.Context, hostclaim.Observed) (park.View, error) { |
| - return func(_ context.Context, obs hostclaim.Observed) (park.View, error) { |
| - if obs.Absent || obs.Claim == nil || obs.Claim.Holder == nil || m == nil { |
| - return park.View{}, park.ErrUnsafe |
| - } |
| - var authoritative string |
| - switch role { |
| - case roleTarget: |
| - authoritative = m.From |
| - case roleSource: |
| - authoritative = m.To |
| - default: |
| - return park.View{}, park.ErrUnsafe |
| - } |
| - location := strings.TrimSuffix(obs.Claim.Holder.Location, "/") |
| - return park.View{ClaimETag: obs.ETag, Region: region, AuthoritativeRegion: authoritative, |
| - GenerationOf: func(r park.Ref) (string, bool) { return landingGenerationOf(location, r.Key) }}, nil |
| - } |
| -} |
| - |
| // landingMarkerGeneration is what landingGenerationOf answers for the |
| // location's generation.json. The marker is the one key every generation |
| // reuses (§5.1): the probe's admission supersedes an earlier stay's marker |
| @@ -132,35 +130,37 @@ func isHexString(s string) bool { |
| return s != "" |
| } |
| -// engine is the park engine over this box's own bucket: the wired leases, |
| -// and the view and authorizer Plan 5 supplies, or the stand-ins. |
| +// engine is the park engine over this box's own bucket with the claim's |
| +// leases and view, and the claim's authorizer behind this install's own |
| +// check (the arming requires all three). |
| func (ir *installRun) engine(vs lease.VersionedStore) park.Engine { |
| cfg := ir.cfg() |
| - view, auth := cfg.ParkView, cfg.ParkAuthorize |
| - if view == nil { |
| - view = standInView(cfg.Region, ir.src.Role, ir.rec.Move) |
| - } |
| - if auth == nil { |
| - auth = ir.authorizeObligation |
| - } |
| return park.Engine{Store: vs, Claims: ir.s, Bucket: cfg.OwnBucket, Region: cfg.Region, Leases: cfg.ParkLeases, |
| - Now: cfg.Now, NewID: obligationID, View: view, Authorize: auth, After: cfg.parkFault} |
| + Now: cfg.Now, NewID: obligationID, View: cfg.ParkView, Authorize: movesArmAuthorize(cfg.ParkAuthorize, ir.authorizeObligation), After: cfg.parkFault} |
| } |
| // lease is the own-location lease the build holds (§7.6 checking: "hold it |
| // through published"), kept in the worker's per-host map across passes and |
| // re-acquired after a restart. waiting is true when another holder has it. |
| func (ir *installRun) lease(ctx context.Context, holder hostclaim.Holder) (held park.HeldLease, waiting bool, err error) { |
| - cfg := ir.cfg() |
| - host := ir.rec.Host |
| - ir.w.mu.Lock() |
| - held = ir.w.landLeases[host] |
| - ir.w.mu.Unlock() |
| + return acquireOwnLease(ctx, ir.w, ir.rec.Host, holder) |
| +} |
| + |
| +// acquireOwnLease takes host's own-location lease in this box's own bucket, |
| +// or answers the one w already holds while it still checks. A landing takes |
| +// it at checking (spec §7.6) and keeps it through published; its residue |
| +// parks run under it (ParkRunHeld, M5). waiting is true when another holder |
| +// has it. |
| +func acquireOwnLease(ctx context.Context, w *movesWorker, host string, holder hostclaim.Holder) (held park.HeldLease, waiting bool, err error) { |
| + cfg := w.cfg |
| + w.mu.Lock() |
| + held = w.landLeases[host] |
| + w.mu.Unlock() |
| if held != nil { |
| if err := held.Check(ctx); err == nil { |
| return held, false, nil |
| } |
| - ir.releaseLease() |
| + releaseOwnLease(w, host) |
| } |
| held, err = cfg.OwnLease(ctx, cfg.OwnBucket, strings.TrimSuffix(holder.Location, "/")) |
| if err != nil { |
| @@ -171,12 +171,12 @@ func (ir *installRun) lease(ctx context.Context, holder hostclaim.Holder) (held |
| } |
| return nil, false, fmt.Errorf("the location's lease: %w", err) |
| } |
| - ir.w.mu.Lock() |
| - if ir.w.landLeases == nil { |
| - ir.w.landLeases = map[string]park.HeldLease{} |
| + w.mu.Lock() |
| + if w.landLeases == nil { |
| + w.landLeases = map[string]park.HeldLease{} |
| } |
| - ir.w.landLeases[host] = held |
| - ir.w.mu.Unlock() |
| + w.landLeases[host] = held |
| + w.mu.Unlock() |
| return held, false, nil |
| } |
diff --git a/internal/host/movesparkfakes_test.go b/internal/host/movesparkfakes_test.go| new file mode 100644 |
| index 000000000..fbaca8928 |
| --- /dev/null |
| +++ b/internal/host/movesparkfakes_test.go |
| @@ -0,0 +1,76 @@ |
| +package host |
| + |
| +import ( |
| + "context" |
| + "strings" |
| + |
| + "github.com/carlosframework/platform/internal/hostclaim" |
| + "github.com/carlosframework/platform/internal/park" |
| + "github.com/carlosframework/platform/internal/provision" |
| + "github.com/carlosframework/platform/internal/release" |
| +) |
| + |
| +// The park fakes the moves tests inject (Plan 5 Task 9a). Production has no |
| +// stand-in for the claim's view or authorizer; these are the rules the |
| +// stand-ins had, kept for the tests that do not exercise the real view |
| +// (movesParkView, movessources_test.go). |
| + |
| +// standInView is the tests' park view: the location's objects classified by |
| +// key, seen from region, and the other side of the move authoritative, which |
| +// §5.1's table says it is before this side's probe is admitted (every install |
| +// phase). role names this side (ruling J24): a landing (roleTarget) works |
| +// while the source's region, From, is authoritative; a dirty return |
| +// (roleSource) while the target's, To, is. |
| +func standInView(region, role string, m *provision.MoveState) func(context.Context, hostclaim.Observed) (park.View, error) { |
| + return func(_ context.Context, obs hostclaim.Observed) (park.View, error) { |
| + if obs.Absent || obs.Claim == nil || obs.Claim.Holder == nil || m == nil { |
| + return park.View{}, park.ErrUnsafe |
| + } |
| + var authoritative string |
| + switch role { |
| + case roleTarget: |
| + authoritative = m.From |
| + case roleSource: |
| + authoritative = m.To |
| + default: |
| + return park.View{}, park.ErrUnsafe |
| + } |
| + location := strings.TrimSuffix(obs.Claim.Holder.Location, "/") |
| + return park.View{ClaimETag: obs.ETag, Region: region, AuthoritativeRegion: authoritative, |
| + GenerationOf: func(r park.Ref) (string, bool) { return landingGenerationOf(location, r.Key) }}, nil |
| + } |
| +} |
| + |
| +// recordView is standInView over the record's move as the store holds it |
| +// when the engine asks. |
| +func recordView(s release.Store, region, role string) func(context.Context, hostclaim.Observed) (park.View, error) { |
| + return func(ctx context.Context, obs hostclaim.Observed) (park.View, error) { |
| + if obs.Absent || obs.Claim == nil || obs.Claim.Holder == nil { |
| + return park.View{}, park.ErrUnsafe |
| + } |
| + h := obs.Claim.Holder |
| + rec, err := provision.GetRecord(ctx, s, h.Account, h.App, obs.Host) |
| + if err != nil { |
| + return park.View{}, err |
| + } |
| + return standInView(region, role, rec.Move)(ctx, obs) |
| + } |
| +} |
| + |
| +// allowParkAuthorize is the tests' claim authorizer: it allows every chore, |
| +// so each arm's own operation check (movesArmAuthorize) decides alone. |
| +func allowParkAuthorize(context.Context, hostclaim.Observed, hostclaim.Obligation) error { return nil } |
| + |
| +// allowLocalAuthorize is the tests' claim Plan 12 local authorizer (M9). |
| +func allowLocalAuthorize(context.Context, *hostclaim.Claim, hostclaim.Obligation) error { return nil } |
| + |
| +// armParkFakes arms c's park hooks with the tests' fakes: leases, the |
| +// record-backed stand-in view for region and role, the allowing claim |
| +// authorizer and claim Plan 12's pieces (testRunHeld, allowLocalAuthorize). |
| +func armParkFakes(c *moveCfg, s release.Store, leases park.Leases, region, role string) { |
| + c.ParkLeases = leases |
| + c.ParkView = recordView(s, region, role) |
| + c.ParkAuthorize = allowParkAuthorize |
| + c.ParkRunHeld = testRunHeld |
| + c.ParkLocalAuthorize = allowLocalAuthorize |
| +} |
diff --git a/internal/host/movesrecover.go b/internal/host/movesrecover.go| index a1fd78401..5fc61d7a6 100644 |
| --- a/internal/host/movesrecover.go |
| +++ b/internal/host/movesrecover.go |
| @@ -154,8 +154,9 @@ type recoveryProbeRequest struct { |
| // recoverySourceArmed reports whether this box can run the source's |
| // recovery: every hook a dirty return's install and the open call is wired, |
| -// the park engine's leases included. In production ParkLeases is not |
| -// (ruling G17), so this answers false there (J18). |
| +// the claim's park hooks and claim Plan 12's RunHeld included (landingArmed). |
| +// In production armMovesParks sets those only with claim Plan 12, so this |
| +// answers false there until it lands (J18). |
| func recoverySourceArmed(cfg moveCfg) bool { |
| return landingArmed(cfg) && cfg.DataDir != "" && cfg.HandHold != nil && cfg.ReleaseHeld != nil && cfg.Timers != nil |
| } |
diff --git a/internal/host/movesrecover_test.go b/internal/host/movesrecover_test.go| index f0174bb47..8fa74c917 100644 |
| --- a/internal/host/movesrecover_test.go |
| +++ b/internal/host/movesrecover_test.go |
| @@ -116,6 +116,7 @@ func (h *recoverHarness) sourceCfg(t *testing.T) moveCfg { |
| } |
| c.Guard = hostguard.Guard{DataDir: h.sdir, LockDir: h.lockDir} |
| c.Local = park.Local{DataDir: h.sdir, Box: "ie-box"} |
| + c.ParkView = recordView(h.s, "ie", roleSource) |
| c.RenderHeld = func(_ context.Context, _ release.Store, rt registry.Route, op string) (appconfig.HeldRender, error) { |
| h.srcRends++ |
| if rb := h.read(t, p1Host).Move.Rollback; rb == nil || op != rb.ID || rt.HoldOp != rb.ID { |
diff --git a/internal/host/movesrevert.go b/internal/host/movesrevert.go| index dfa54e91d..0689f18a9 100644 |
| --- a/internal/host/movesrevert.go |
| +++ b/internal/host/movesrevert.go |
| @@ -158,10 +158,11 @@ func revertSourceArmed(cfg moveCfg) bool { |
| } |
| // revertLaneArmed reports whether this box can run the target region's lane |
| -// park: the park engine's leases and this box's own bucket. In production |
| -// ParkLeases is not wired (ruling G17), so this answers false there. |
| +// park: the claim's park leases, view and authorizer, and this box's own |
| +// bucket. In production armMovesParks sets the three only with claim Plan |
| +// 12, so this answers false there until it lands. |
| func revertLaneArmed(cfg moveCfg) bool { |
| - return cfg.Box != "" && cfg.Region != "" && cfg.OwnBucket != "" && cfg.ParkLeases != nil && cfg.Versioned != nil |
| + return cfg.Box != "" && cfg.Region != "" && cfg.OwnBucket != "" && parkHooksArmed(cfg) && cfg.Versioned != nil |
| } |
| // restatusHold is moveCfg.RestatusHold in production: an op's own hold |
| @@ -860,11 +861,10 @@ func (w *movesWorker) sweepRevertJournal(dataDir, host, op string, j *moves.Jour |
| // hostclaim.Abort queued, Plan 5 Task 1) and a committed landing |
| // (landing/committed; the park hostclaim.Commit queued, Plan 5 Task 3, §7.9 |
| // step 4, called from the committed arm). No phases means any phase of |
| -// kind. The view and the authorizer are Plan 5's when wired, else the |
| -// stand-ins: the source's region authoritative (see the view below), and |
| -// only this move's lane park under kind and its phases, with this box not |
| -// executing the host unless the landing is committed (the target serves it |
| -// then). |
| +// kind. The view and the authorizer are the claim's (MoveParks), the |
| +// authorizer behind this lane's own check: only this move's lane park under |
| +// kind and its phases, with this box not executing the host unless the |
| +// landing is committed (the target serves it then). |
| func (w *movesWorker) laneParkTarget(ctx context.Context, s release.Store, rec provision.Record, kind string, phases ...string) error { |
| cfg := w.cfg |
| m := rec.Move |
| @@ -908,28 +908,19 @@ func (w *movesWorker) laneParkTarget(ctx context.Context, s release.Store, rec p |
| return err |
| } |
| committed := kind == hostclaim.KindLanding |
| - view, auth := cfg.ParkView, cfg.ParkAuthorize |
| - if view == nil { |
| - // The stand-in names From authoritative, after a commit too. For a |
| - // lane, which is never the holder's location, the engine reads the |
| - // authoritative region only to decide whether it must protect the |
| - // location's live generations (park.protected), and a stand-in has |
| - // no marker to protect them with. Plan 5's view (Task 9) answers |
| - // the target's region after the commit, with its marker. |
| - view = standInView(cfg.Region, roleTarget, m) |
| - } |
| - if auth == nil { |
| - auth = func(_ context.Context, obs hostclaim.Observed, o hostclaim.Obligation) error { |
| - c := obs.Claim |
| - if obs.Absent || c == nil || !inOp(c.Op) || (!committed && c.Op.Exec == cfg.Box) || |
| - o.Op != id || o.Kind != "park-replica" || o.Region != cfg.Region || o.Park == nil || o.Park.Location != lane { |
| - return fmt.Errorf("%w: not this move's lane park", park.ErrUnsafe) |
| - } |
| - return nil |
| + // The claim's view (movesParkView) names the target's region |
| + // authoritative after the commit, with its marker, and the source's |
| + // before it; the claim's authorizer runs behind this lane's own check. |
| + own := func(_ context.Context, obs hostclaim.Observed, o hostclaim.Obligation) error { |
| + c := obs.Claim |
| + if obs.Absent || c == nil || !inOp(c.Op) || (!committed && c.Op.Exec == cfg.Box) || |
| + o.Op != id || o.Kind != "park-replica" || o.Region != cfg.Region || o.Park == nil || o.Park.Location != lane { |
| + return fmt.Errorf("%w: not this move's lane park", park.ErrUnsafe) |
| } |
| + return nil |
| } |
| e := park.Engine{Store: vs, Claims: s, Bucket: cfg.OwnBucket, Region: cfg.Region, Leases: cfg.ParkLeases, |
| - Now: w.now, NewID: obligationID, View: view, Authorize: auth, After: cfg.parkFault} |
| + Now: w.now, NewID: obligationID, View: cfg.ParkView, Authorize: movesArmAuthorize(cfg.ParkAuthorize, own), After: cfg.parkFault} |
| for _, o := range chores { |
| err := e.Run(ctx, park.Work{Host: host, ID: o.ID, Op: id}) |
| switch { |
diff --git a/internal/host/movesrevert_test.go b/internal/host/movesrevert_test.go| index dd8dfd456..a35434fdb 100644 |
| --- a/internal/host/movesrevert_test.go |
| +++ b/internal/host/movesrevert_test.go |
| @@ -162,7 +162,7 @@ func revertRendered() provision.MoveInputs { |
| func (h *revertHarness) laneCfg() moveCfg { |
| c := h.target() |
| c.DataDir = h.tdir |
| - c.ParkLeases = h.tleases |
| + armParkFakes(&c, h.s, h.tleases, c.Region, roleTarget) |
| return c |
| } |
diff --git a/internal/host/movesrollback.go b/internal/host/movesrollback.go| index d52afdedc..33b81c81c 100644 |
| --- a/internal/host/movesrollback.go |
| +++ b/internal/host/movesrollback.go |
| @@ -248,7 +248,7 @@ func (w *movesWorker) rollbackClaim(ctx context.Context, s release.Store, rec pr |
| // included. In production ParkLeases is not (ruling G17), so this answers |
| // false there (J18). |
| func rollbackTargetArmed(cfg moveCfg) bool { |
| - return cfg.Box != "" && cfg.DataDir != "" && cfg.Guard.DataDir != "" && cfg.Local.DataDir != "" && cfg.ParkLeases != nil && |
| + return cfg.Box != "" && cfg.DataDir != "" && cfg.Guard.DataDir != "" && cfg.Local.DataDir != "" && parkHooksArmed(cfg) && |
| cfg.HandHold != nil && cfg.Fence != nil && cfg.TimersDrained != nil && cfg.Evidence != nil && cfg.FinalCheckpoint != nil && |
| cfg.LocalPosition != nil && cfg.Timers != nil && cfg.FilesDir != nil && cfg.Route != nil && cfg.Versioned != nil && |
| cfg.StagingLease != nil && cfg.Staging != nil && cfg.DeleteHeld != nil && cfg.SecretsPub != nil |
| @@ -1163,30 +1163,26 @@ func (r *rollbackRun) remote(ctx context.Context, obs hostclaim.Observed, j *mov |
| return true, nil |
| } |
| -// engine is the park engine over this box's own bucket: the wired leases, and |
| -// the view and authorizer Plan 5 supplies, or the stand-ins (the view with |
| -// the source's region authoritative, which it is again after the rollback). |
| +// engine is the park engine over this box's own bucket with the claim's |
| +// leases and view, and the claim's authorizer behind this rollback's own |
| +// check (rollbackTargetArmed requires all three; nothing stands in). |
| func (r *rollbackRun) engine(vs lease.VersionedStore) park.Engine { |
| cfg := r.cfg() |
| - view, auth := cfg.ParkView, cfg.ParkAuthorize |
| - if view == nil { |
| - view = standInView(cfg.Region, roleTarget, r.rec.Move) |
| - } |
| - if auth == nil { |
| - auth = func(_ context.Context, obs hostclaim.Observed, o hostclaim.Obligation) error { |
| - if obs.Absent { |
| - return park.ErrUnsafe |
| - } |
| - return r.authorizeChore(obs.Claim, o) |
| + own := func(_ context.Context, obs hostclaim.Observed, o hostclaim.Obligation) error { |
| + if obs.Absent { |
| + return park.ErrUnsafe |
| } |
| + return r.authorizeChore(obs.Claim, o) |
| } |
| return park.Engine{Store: vs, Claims: r.s, Bucket: cfg.OwnBucket, Region: cfg.Region, Leases: cfg.ParkLeases, |
| - Now: r.now, NewID: obligationID, View: view, Authorize: auth, After: cfg.parkFault} |
| + Now: r.now, NewID: obligationID, View: cfg.ParkView, Authorize: movesArmAuthorize(cfg.ParkAuthorize, own), After: cfg.parkFault} |
| } |
| -// authorizeChore is the stand-in rule until Plan 5 (ruling G17): a chore is |
| +// authorizeChore is this rollback's own operation check (M17): a chore is |
| // run only when it is this op's, the claim is still this rollback's with this |
| -// box no longer executing, and the chore is this region's or this box's. |
| +// box no longer executing, and the chore is this region's or this box's. The |
| +// engine runs it before the claim's authorizer; park.Local runs it alone |
| +// while claim Plan 12's local authorizer is missing (installation). |
| func (r *rollbackRun) authorizeChore(c *hostclaim.Claim, o hostclaim.Obligation) error { |
| cfg := r.cfg() |
| if r.ours(c) != nil || o.Op != r.g.ID || c.Op.Exec == cfg.Box { |
| @@ -1263,11 +1259,22 @@ func (r *rollbackRun) installation(ctx context.Context, ours bool) (string, erro |
| local.Box = cfg.Box |
| } |
| if local.Authorize == nil { |
| + // park.Local's authorizer: claim Plan 12's local authorizer |
| + // (ParkLocalAuthorize) behind this rollback's own check. The |
| + // claim's engine authorizer refuses a park-local (M9), so it is |
| + // never asked here. Until claim Plan 12 lands this rollback's own |
| + // rule stands alone: a named exception, the second of the two. |
| + // claim Plan 12 F: drop the fallback once ParkLocalAuthorize is |
| + // always wired. |
| + plan12 := cfg.ParkLocalAuthorize |
| local.Authorize = func(ctx context.Context, c *hostclaim.Claim, o hostclaim.Obligation) error { |
| - if auth := cfg.ParkAuthorize; auth != nil { |
| - return auth(ctx, hostclaim.Observed{Host: host, Claim: c}, o) |
| + if err := r.authorizeChore(c, o); err != nil { |
| + return err |
| + } |
| + if plan12 != nil { |
| + return plan12(ctx, c, o) |
| } |
| - return r.authorizeChore(c, o) |
| + return nil |
| } |
| } |
| if err := r.fault("before-rb-local-retire"); err != nil { |
diff --git a/internal/host/movessources_test.go b/internal/host/movessources_test.go| index 59d4c139a..a012b4246 100644 |
| --- a/internal/host/movessources_test.go |
| +++ b/internal/host/movessources_test.go |
| @@ -547,3 +547,106 @@ func TestRegisterProductionMovesParksNeedsBothSwitches(t *testing.T) { |
| t.Fatalf("no placement region: %v", err) |
| } |
| } |
| + |
| +// TestLandingResidueParkUnderItsOwnLease (M5): the landing takes its lease on |
| +// the location at checking and holds it through published (spec §7.6). Over |
| +// the real AggregateLeases and the moves lease set, the residue park's |
| +// scope resolves to that same lock, so Engine.Run would wait on the landing |
| +// itself; claim Plan 12's RunHeld runs the chores under the held lease and |
| +// the landing goes on to restored. |
| +func TestLandingResidueParkUnderItsOwnLease(t *testing.T) { |
| + location := "data-uk/" + cutRoot |
| + t.Run("under RunHeld", func(t *testing.T) { |
| + h := newLandingHarness(t) |
| + h.flip(t) |
| + keys := h.residue(t) |
| + h.parkLeases = hostbox.AggregateLeases{Leases: h.leases, Resolve: movesLeaseSet("data-uk")} |
| + runs := 0 |
| + h.runHeld = func(ctx context.Context, e park.Engine, w park.Work, held park.HeldLease) error { |
| + runs++ |
| + if got := h.leases.holding(); len(got) != 1 || got[0] != location { |
| + t.Errorf("a residue park ran while the landing held %v, want %s", got, location) |
| + } |
| + return testRunHeld(ctx, e, w, held) |
| + } |
| + h.land(t) |
| + h.installed(t) |
| + if runs == 0 { |
| + t.Fatal("no residue park ran") |
| + } |
| + for _, k := range keys { |
| + if _, ok := h.f.buckets["data-uk"][k]; ok && !strings.HasSuffix(k, "/generation.json") { |
| + t.Fatalf("%s is still at the location", k) |
| + } |
| + } |
| + }) |
| + t.Run("Engine.Run waits on the landing's own lease", func(t *testing.T) { |
| + h := newLandingHarness(t) |
| + h.flip(t) |
| + keys := h.residue(t) |
| + h.parkLeases = hostbox.AggregateLeases{Leases: h.leases, Resolve: movesLeaseSet("data-uk")} |
| + h.runHeld = func(ctx context.Context, e park.Engine, w park.Work, _ park.HeldLease) error { return e.Run(ctx, w) } |
| + for i := 0; i < 3; i++ { |
| + h.land(t) |
| + } |
| + if m := h.read(t, p1Host).Move; m.LandingPhase != provision.LandParking { |
| + t.Fatalf("the landing went past parking without RunHeld: %+v", m) |
| + } |
| + if got := h.leases.holding(); len(got) != 1 || got[0] != location { |
| + t.Fatalf("the landing holds %v", got) |
| + } |
| + for _, k := range keys { |
| + if _, ok := h.f.buckets["data-uk"][k]; !ok { |
| + t.Fatalf("%s was parked under a lease nobody held", k) |
| + } |
| + } |
| + }) |
| +} |
| + |
| +// TestNoArmRunsWithoutTheThreeParkHooks (M17): every moves arm requires the |
| +// claim's park leases, view and authorizer, the landing (and the recovery's |
| +// install) claim Plan 12's RunHeld too, the finalization its local |
| +// authorizer; production's own wiring sets none of them. |
| +func TestNoArmRunsWithoutTheThreeParkHooks(t *testing.T) { |
| + three := map[string]func(*moveCfg){ |
| + "no park leases": func(c *moveCfg) { c.ParkLeases = nil }, |
| + "no park view": func(c *moveCfg) { c.ParkView = nil }, |
| + "no park authorizer": func(c *moveCfg) { c.ParkAuthorize = nil }, |
| + } |
| + runHeld := func(c *moveCfg) { c.ParkRunHeld = nil } |
| + local := func(c *moveCfg) { c.ParkLocalAuthorize = nil } |
| + arms := []struct { |
| + name string |
| + cfg func(t *testing.T) moveCfg |
| + armed func(moveCfg) bool |
| + extra map[string]func(*moveCfg) |
| + }{ |
| + {"landing", func(t *testing.T) moveCfg { return newLandingHarness(t).lcfg }, landingArmed, map[string]func(*moveCfg){"no claim Plan 12 RunHeld": runHeld}}, |
| + {"rollback target", func(t *testing.T) moveCfg { h := newRollbackHarness(t); return h.rollbackCfg(t) }, rollbackTargetArmed, nil}, |
| + {"lane park (revert, abort, committed)", func(t *testing.T) moveCfg { return newRevertHarness(t).laneCfg() }, revertLaneArmed, nil}, |
| + {"recovery", func(t *testing.T) moveCfg { h := newRecoverHarness(t, true); return h.sourceCfg(t) }, recoverySourceArmed, map[string]func(*moveCfg){"no claim Plan 12 RunHeld": runHeld}}, |
| + {"source finalization", func(t *testing.T) moveCfg { return newFinalHarness(t).cfg }, sourceFinalArmed, map[string]func(*moveCfg){"no claim Plan 12 local authorizer": local}}, |
| + } |
| + for _, a := range arms { |
| + t.Run(a.name, func(t *testing.T) { |
| + c := a.cfg(t) |
| + if !a.armed(c) { |
| + t.Fatal("fixture: the arm is not armed with every hook") |
| + } |
| + for name, unarm := range three { |
| + u := c |
| + unarm(&u) |
| + if a.armed(u) { |
| + t.Fatalf("%s: armed", name) |
| + } |
| + } |
| + for name, unarm := range a.extra { |
| + u := c |
| + unarm(&u) |
| + if a.armed(u) { |
| + t.Fatalf("%s: armed", name) |
| + } |
| + } |
| + }) |
| + } |
| +} |