carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package host
2
3import (
4 "context"
5 "encoding/json"
6 "errors"
7 "sort"
8 "strings"
9 "sync"
10 "testing"
11 "time"
12
13 "github.com/carlosframework/platform/internal/activator"
14 "github.com/carlosframework/platform/internal/hostbox"
15 "github.com/carlosframework/platform/internal/hostclaim"
16 "github.com/carlosframework/platform/internal/hostwrite"
17 "github.com/carlosframework/platform/internal/lease"
18 "github.com/carlosframework/platform/internal/lease/leasetest"
19 "github.com/carlosframework/platform/internal/moves"
20 "github.com/carlosframework/platform/internal/park"
21 "github.com/carlosframework/platform/internal/provision"
22 "github.com/carlosframework/platform/internal/release"
23)
24
25// The moves park providers (Plan 5 Task 9a): the view, the claim's lease set
26// over moves' scopes, the wake-source gate (Q1), the registration and the
27// arming from the claim's MoveParks.
28
29const (
30 vwHost = "vw.example"
31 vwAcct = "acct"
32 vwApp = "app"
33 vwMove = "mv_vwvwvwvwvwvwvwvwvwvwvwvwvw"
34 vwRb = "rb_vwvwvwvwvwvwvwvwvwvwvwvwvw"
35)
36
37var vwCreated = time.Date(2026, 9, 1, 0, 0, 0, 0, time.UTC)
38
39// vwFixture is one host's claim observation, its record and its data
40// bucket, for the view.
41type vwFixture struct {
42 inc, location string
43 rec *provision.Record
44 bucket *leasetest.VersionedBucket
45 reads, lists int
46}
47
48func newVwFixture(t *testing.T) *vwFixture {
49 t.Helper()
50 inc, err := hostclaim.Incarnation(vwAcct, vwApp, vwHost, vwCreated)
51 if err != nil {
52 t.Fatal(err)
53 }
54 f := &vwFixture{inc: inc, location: "instances/" + vwHost + "/" + inc, bucket: leasetest.NewVersionedBucket()}
55 f.bucket.AddBucket("data")
56 f.rec = &provision.Record{Schema: 1, Host: vwHost, App: vwApp, Account: vwAcct, CreatedAt: vwCreated, Region: "ie"}
57 return f
58}
59
60func (f *vwFixture) records() MovesRecordReader {
61 return func(_ context.Context, acct, app, host string) (*provision.Record, error) {
62 f.reads++
63 if acct != vwAcct || app != vwApp || host != vwHost || f.rec == nil {
64 return nil, errors.New("no such record")
65 }
66 cp := *f.rec
67 return &cp, nil
68 }
69}
70
71// countingReader counts the view's listings, to show a non-authoritative
72// region reads nothing.
73type countingReader struct {
74 *leasetest.VersionedBucket
75 f *vwFixture
76}
77
78func (r countingReader) ListVersions(ctx context.Context, bucket, prefix string) ([]lease.ObjectVersion, error) {
79 r.f.lists++
80 return r.VersionedBucket.ListVersions(ctx, bucket, prefix)
81}
82
83func (f *vwFixture) view(region string) func(context.Context, hostclaim.Observed) (park.View, error) {
84 return movesParkView(f.records(), countingReader{f.bucket, f}, "data", region)
85}
86
87// holder is the fixture's holder at generation gen with admissions, each
88// "generation@region".
89func (f *vwFixture) holder(gen string, admissions ...string) *hostclaim.Holder {
90 h := &hostclaim.Holder{Family: "instance", Key: provision.RecordKey(vwAcct, vwApp, vwHost), Account: vwAcct, App: vwApp,
91 Incarnation: f.inc, CreatedAt: vwCreated, Location: f.location, Generation: gen, Stamp: 3, Session: 3}
92 for i, a := range admissions {
93 g, r, _ := strings.Cut(a, "@")
94 h.Admissions = append(h.Admissions, hostclaim.AdmissionRecord{Session: uint64(i + 1), Box: r + "-box", Generation: g, Mode: "cold", Region: r})
95 }
96 return h
97}
98
99func vwObs(c *hostclaim.Claim) hostclaim.Observed {
100 c.Host = vwHost
101 return hostclaim.Observed{Host: vwHost, ETag: "etag-1", Claim: c}
102}
103
104func (f *vwFixture) move(state string) {
105 f.rec.Move = &provision.MoveState{ID: vwMove, Incarnation: f.inc, From: "ie", To: "uk", State: state}
106}
107
108// marker writes generation.json at the location naming gen, pinning a
109// snapshot of it, and returns the marker's version and the snapshot's.
110func (f *vwFixture) marker(t *testing.T, gen, inc string) (markerVersion, snapshot string) {
111 t.Helper()
112 snapKey := f.location + "/g-" + gen + "/0009/0000000000000001-0000000000000005.ltx"
113 snapshot = f.bucket.Put("data", snapKey, []byte("snapshot"))
114 body, err := json.Marshal(map[string]any{"op": gen, "incarnation": inc, "host": vwHost, "box": "uk-box", "installed_at": vwCreated,
115 "manifest_sha256": "x", "snapshot_key": snapKey, "snapshot_version": snapshot, "snapshot_txid": 5, "snapshot_crc64": 1})
116 if err != nil {
117 t.Fatal(err)
118 }
119 return f.bucket.Put("data", f.location+"/generation.json", body), snapshot
120}
121
122// TestMoveParkViewFollowsTheAuthoritativeRegionTable: one row per claim
123// state of the spec's table (§5.1), seen from a region that is neither, so
124// only the authoritative region is asked of the view, plus the refusals.
125func TestMoveParkViewFollowsTheAuthoritativeRegionTable(t *testing.T) {
126 type row struct {
127 name string
128 claim func(f *vwFixture) *hostclaim.Claim
129 rec func(f *vwFixture)
130 want string // "" wants a refusal
131 }
132 op := func(kind, phase string) hostclaim.Op {
133 return hostclaim.Op{Kind: kind, ID: vwMove, Phase: phase, Exec: "box"}
134 }
135 rows := []row{
136 {"free", func(f *vwFixture) *hostclaim.Claim { return &hostclaim.Claim{} }, nil, movesNoLiveRegion},
137 {"creating", func(f *vwFixture) *hostclaim.Claim {
138 return &hostclaim.Claim{Holder: f.holder(""), Op: hostclaim.Op{Kind: hostclaim.KindCreating, ID: "op"}}
139 }, nil, movesNoLiveRegion},
140 {"deleting", func(f *vwFixture) *hostclaim.Claim {
141 return &hostclaim.Claim{Holder: f.holder("cur", "cur@ie"), Op: hostclaim.Op{Kind: hostclaim.KindDeleting, ID: "op"}}
142 }, nil, movesNoLiveRegion},
143 {"parking", func(f *vwFixture) *hostclaim.Claim {
144 return &hostclaim.Claim{Holder: f.holder("cur", "cur@ie"), Op: hostclaim.Op{Kind: hostclaim.KindParking, ID: "op"}}
145 }, nil, movesNoLiveRegion},
146 {"purging", func(f *vwFixture) *hostclaim.Claim {
147 return &hostclaim.Claim{Holder: f.holder("cur", "cur@ie"), Op: hostclaim.Op{Kind: hostclaim.KindPurging, ID: "op"}}
148 }, nil, movesNoLiveRegion},
149 {"serving: the admission's region", func(f *vwFixture) *hostclaim.Claim {
150 return &hostclaim.Claim{Holder: f.holder("cur", "old@ie", "cur@uk"), Op: hostclaim.Op{Kind: hostclaim.KindServing, Exec: "uk-box"}}
151 }, nil, "uk"},
152 {"serving: a legacy holder takes the record's region", func(f *vwFixture) *hostclaim.Claim {
153 return &hostclaim.Claim{Holder: f.holder(""), Op: hostclaim.Op{Kind: hostclaim.KindServing, Exec: "box"}}
154 }, nil, "ie"},
155 {"serving: no admission and no record region", func(f *vwFixture) *hostclaim.Claim {
156 return &hostclaim.Claim{Holder: f.holder(""), Op: hostclaim.Op{Kind: hostclaim.KindServing, Exec: "box"}}
157 }, func(f *vwFixture) { f.rec.Region = "" }, ""},
158 {"pushing", func(f *vwFixture) *hostclaim.Claim {
159 return &hostclaim.Claim{Holder: f.holder("cur", "cur@uk"), Op: hostclaim.Op{Kind: hostclaim.KindPushing, ID: "push"}}
160 }, nil, "uk"},
161 {"moving before the flip", func(f *vwFixture) *hostclaim.Claim {
162 return &hostclaim.Claim{Holder: f.holder("cur", "cur@ie"), Op: op(hostclaim.KindMoving, hostclaim.PhaseMoving)}
163 }, func(f *vwFixture) { f.move(provision.MoveCutting) }, "ie"},
164 {"reverting", func(f *vwFixture) *hostclaim.Claim {
165 return &hostclaim.Claim{Holder: f.holder("cur", "cur@ie"), Op: op(hostclaim.KindReverting, "")}
166 }, func(f *vwFixture) { f.move(provision.MoveReverting) }, "ie"},
167 {"landing before the target's probe is admitted", func(f *vwFixture) *hostclaim.Claim {
168 return &hostclaim.Claim{Holder: f.holder("cur", "cur@ie"), Op: op(hostclaim.KindLanding, provision.LandBuilding)}
169 }, func(f *vwFixture) { f.move(provision.MoveLanding) }, "ie"},
170 {"landing from the target's probe's admission", func(f *vwFixture) *hostclaim.Claim {
171 return &hostclaim.Claim{Holder: f.holder(vwMove, "cur@ie", vwMove+"@uk"), Op: op(hostclaim.KindLanding, provision.LandProbing)}
172 }, func(f *vwFixture) { f.move(provision.MoveLanding) }, "uk"},
173 {"landing committed", func(f *vwFixture) *hostclaim.Claim {
174 return &hostclaim.Claim{Holder: f.holder("fresh2", "cur@ie", vwMove+"@uk", "fresh2@uk"), Op: op(hostclaim.KindLanding, hostclaim.PhaseCommitted)}
175 }, func(f *vwFixture) { f.move(provision.MoveCommitted) }, "uk"},
176 {"landing committed with no admission in the target", func(f *vwFixture) *hostclaim.Claim {
177 return &hostclaim.Claim{Holder: f.holder("cur", "cur@ie"), Op: op(hostclaim.KindLanding, hostclaim.PhaseCommitted)}
178 }, func(f *vwFixture) { f.move(provision.MoveCommitted) }, ""},
179 {"landing closed record first (M4)", func(f *vwFixture) *hostclaim.Claim {
180 return &hostclaim.Claim{Holder: f.holder(vwMove, "cur@ie", vwMove+"@uk"), Op: op(hostclaim.KindLanding, hostclaim.PhaseCommitted)}
181 }, func(f *vwFixture) {
182 f.rec.LastMove = &provision.MoveOutcome{ID: vwMove, Outcome: "done", At: vwCreated}
183 }, "uk"},
184 {"restoring, clean, from the return CAS", func(f *vwFixture) *hostclaim.Claim {
185 return &hostclaim.Claim{Holder: f.holder("cur", "cur@ie", vwMove+"@uk"), Op: op(hostclaim.KindRestoring, hostclaim.PhaseClaimed)}
186 }, func(f *vwFixture) { f.move(provision.MoveRollingBack) }, "ie"},
187 {"restoring, dirty, before the source's probe", func(f *vwFixture) *hostclaim.Claim {
188 return &hostclaim.Claim{Holder: f.holder(vwMove, "cur@ie", vwMove+"@uk"), Op: op(hostclaim.KindRestoring, hostclaim.PhaseClaimed)}
189 }, func(f *vwFixture) { f.move(provision.MoveRollingBack) }, "uk"},
190 {"restoring, dirty, from the source's probe's admission", func(f *vwFixture) *hostclaim.Claim {
191 return &hostclaim.Claim{Holder: f.holder(vwRb, "cur@ie", vwMove+"@uk", vwRb+"@ie"), Op: op(hostclaim.KindRestoring, hostclaim.PhaseProbing)}
192 }, func(f *vwFixture) { f.move(provision.MoveRollingBack) }, "ie"},
193 {"restoring before any admission of the generation", func(f *vwFixture) *hostclaim.Claim {
194 return &hostclaim.Claim{Holder: f.holder(""), Op: op(hostclaim.KindRestoring, hostclaim.PhaseReleasing)}
195 }, func(f *vwFixture) { f.move(provision.MoveRollingBack) }, "ie"},
196 {"moving with the generation admitted in the target", func(f *vwFixture) *hostclaim.Claim {
197 return &hostclaim.Claim{Holder: f.holder("cur", "cur@uk"), Op: op(hostclaim.KindMoving, hostclaim.PhaseMoving)}
198 }, func(f *vwFixture) { f.move(provision.MoveCutting) }, ""},
199 {"a move op the record does not carry", func(f *vwFixture) *hostclaim.Claim {
200 return &hostclaim.Claim{Holder: f.holder("cur", "cur@ie"), Op: op(hostclaim.KindLanding, provision.LandBuilding)}
201 }, nil, ""},
202 {"placing", func(f *vwFixture) *hostclaim.Claim {
203 return &hostclaim.Claim{Holder: f.holder("cur", "cur@ie"), Op: hostclaim.Op{Kind: hostclaim.KindPlacing, ID: "op"}}
204 }, nil, ""},
205 {"relocating", func(f *vwFixture) *hostclaim.Claim {
206 return &hostclaim.Claim{Holder: f.holder("cur", "cur@ie"), Op: hostclaim.Op{Kind: "relocating", ID: "op"}}
207 }, nil, ""},
208 }
209 for _, r := range rows {
210 t.Run(r.name, func(t *testing.T) {
211 f := newVwFixture(t)
212 if r.rec != nil {
213 r.rec(f)
214 }
215 obs := vwObs(r.claim(f))
216 v, err := f.view("us")(t.Context(), obs)
217 if r.want == "" {
218 if err == nil || !errors.Is(err, park.ErrUnsafe) {
219 t.Fatalf("view %+v, %v; want a refusal", v, err)
220 }
221 return
222 }
223 if err != nil {
224 t.Fatal(err)
225 }
226 if v.AuthoritativeRegion != r.want || v.Region != "us" || v.ClaimETag != obs.ETag {
227 t.Fatalf("view %+v; want %s authoritative, seen from us", v, r.want)
228 }
229 if v.MarkerKnown || f.lists != 0 {
230 t.Fatalf("a non-authoritative region read the marker (%d listings)", f.lists)
231 }
232 if obs.Claim.Holder != nil && v.GenerationOf == nil {
233 t.Fatal("no classifier for the holder's location")
234 }
235 })
236 }
237}
238
239// TestMoveParkViewReadsTheMarkerByVersion: in the authoritative region the
240// view reads generation.json by the version the listing names and pins the
241// snapshot it names (the installed root); absent is the origin; a marker of
242// another incarnation refuses.
243func TestMoveParkViewReadsTheMarkerByVersion(t *testing.T) {
244 serving := func(f *vwFixture) hostclaim.Observed {
245 return vwObs(&hostclaim.Claim{Holder: f.holder("cur", "cur@uk"), Op: hostclaim.Op{Kind: hostclaim.KindServing, Exec: "uk-box"}})
246 }
247 t.Run("present", func(t *testing.T) {
248 f := newVwFixture(t)
249 mv, snap := f.marker(t, "cur", f.inc)
250 v, err := f.view("uk")(t.Context(), serving(f))
251 if err != nil {
252 t.Fatal(err)
253 }
254 root := park.Ref{Key: f.location + "/g-cur/0009/0000000000000001-0000000000000005.ltx", VersionID: snap}
255 if !v.MarkerKnown || v.MarkerGeneration != "cur" || v.Marker != (park.Ref{Key: f.location + "/generation.json", VersionID: mv}) || !v.Pinned[root] || len(v.Pinned) != 1 {
256 t.Fatalf("view %+v", v)
257 }
258 if g, ok := v.GenerationOf(root); !ok || g != "cur" {
259 t.Fatalf("the root classifies as %q %v", g, ok)
260 }
261 })
262 t.Run("absent is the origin", func(t *testing.T) {
263 f := newVwFixture(t)
264 v, err := f.view("uk")(t.Context(), serving(f))
265 if err != nil || !v.MarkerKnown || v.MarkerGeneration != "" || v.Marker != (park.Ref{}) || len(v.Pinned) != 0 {
266 t.Fatalf("view %+v, %v", v, err)
267 }
268 })
269 t.Run("another incarnation's marker", func(t *testing.T) {
270 f := newVwFixture(t)
271 f.marker(t, "cur", strings.Repeat("e", 64))
272 if v, err := f.view("uk")(t.Context(), serving(f)); !errors.Is(err, park.ErrUnsafe) {
273 t.Fatalf("view %+v, %v", v, err)
274 }
275 })
276 t.Run("a deleted marker", func(t *testing.T) {
277 f := newVwFixture(t)
278 f.marker(t, "cur", f.inc)
279 f.bucket.BareDelete("data", f.location+"/generation.json")
280 if v, err := f.view("uk")(t.Context(), serving(f)); !errors.Is(err, park.ErrUnsafe) {
281 t.Fatalf("view %+v, %v", v, err)
282 }
283 })
284}
285
286// TestMovesScopesResolveThroughTheClaimsLeaseSet (M6): the claim's lease set
287// (hostbox.ReplicaParkLeaseSet, the resolver of the MoveParks engine's
288// AggregateLeases) answers every scope a moves park names with that scope's
289// own lock, and acquired through the real AggregateLeases it takes exactly
290// that lock. The scopes are moves' own: the holder location, the residue
291// park's, the lane and the op's lane.
292func TestMovesScopesResolveThroughTheClaimsLeaseSet(t *testing.T) {
293 inc := strings.Repeat("a", 64)
294 holder := hostclaim.Holder{Location: "instances/" + vwHost + "/" + inc}
295 scopes := []string{
296 "instances/" + vwHost,
297 strings.TrimSuffix(holder.Location, "/"),
298 moves.ResidueScope(holder, vwCreated).Location,
299 "moves/" + vwHost,
300 moves.ScopeLocation(vwHost, vwMove),
301 }
302 for _, scope := range scopes {
303 names, err := hostbox.ReplicaParkLeaseSet(t.Context(), "data-ie", scope)
304 if err != nil || len(names) != 1 || names[0] != scope {
305 t.Fatalf("%s: %v %v", scope, names, err)
306 }
307 leases := &fakeParkLeases{}
308 held, err := hostbox.AggregateLeases{Leases: leases, Resolve: hostbox.ReplicaParkLeaseSet}.Acquire(t.Context(), "data-ie", scope)
309 if err != nil {
310 t.Fatalf("%s: %v", scope, err)
311 }
312 if got := leases.holding(); len(got) != 1 || got[0] != "data-ie/"+scope {
313 t.Fatalf("%s holds %v", scope, got)
314 }
315 held.Close()
316 }
317 for _, scope := range []string{"", "instances", "instances/" + vwHost + "/g-cur", "instances/" + vwHost + "/" + strings.ToUpper(inc),
318 "moves/" + vwHost + "/x/y", "parked/" + vwHost, "instances/../" + vwHost} {
319 if names, err := hostbox.ReplicaParkLeaseSet(t.Context(), "data-ie", scope); err == nil {
320 t.Fatalf("%q answered %v", scope, names)
321 }
322 }
323}
324
325// withMovesWakeSource empties the claim's move-source slots and the moves
326// gate's own record for the test, restoring all three after it. register
327// then registers a stand-in source through registerMovesWakeSource, which
328// opens the gate (movesWakeSourceRegistered).
329func withMovesWakeSource(t *testing.T, register bool) {
330 t.Helper()
331 claimMoveSources.Lock()
332 oldSource, oldParks := claimMoveSources.Source, claimMoveSources.Parks
333 claimMoveSources.Source, claimMoveSources.Parks = nil, nil
334 claimMoveSources.Unlock()
335 movesWakeSource.Lock()
336 oldReg := movesWakeSource.registered
337 movesWakeSource.registered = nil
338 movesWakeSource.Unlock()
339 t.Cleanup(func() {
340 claimMoveSources.Lock()
341 claimMoveSources.Source, claimMoveSources.Parks = oldSource, oldParks
342 claimMoveSources.Unlock()
343 movesWakeSource.Lock()
344 movesWakeSource.registered = oldReg
345 movesWakeSource.Unlock()
346 })
347 if register {
348 if err := registerMovesWakeSource(&parkRegistrationWake{}); err != nil {
349 t.Fatal(err)
350 }
351 }
352}
353
354// renewTable is a lease table behind hostbox.RenewingLeases' Open: one
355// holder per bucket/scope, a second open of a held one refused as
356// lease.Acquire refuses it. Everything above Open (RenewingLeases,
357// AggregateLeases, AcquireHeld, LeaseScope) is the claim's own.
358type renewTable struct {
359 mu sync.Mutex
360 held map[string]bool
361 opens []string
362}
363
364type renewHeld struct {
365 t *renewTable
366 key string
367 lost chan struct{}
368}
369
370func (r *renewTable) open(_ context.Context, bucket, scope, _ string) (hostbox.Renewable, error) {
371 r.mu.Lock()
372 defer r.mu.Unlock()
373 key := bucket + "/" + scope
374 if r.held[key] {
375 return nil, &lease.HeldError{Owner: "another holder"}
376 }
377 if r.held == nil {
378 r.held = map[string]bool{}
379 }
380 r.held[key] = true
381 r.opens = append(r.opens, key)
382 return &renewHeld{t: r, key: key, lost: make(chan struct{})}, nil
383}
384
385func (r *renewTable) holding() []string {
386 r.mu.Lock()
387 defer r.mu.Unlock()
388 var out []string
389 for k, on := range r.held {
390 if on {
391 out = append(out, k)
392 }
393 }
394 sort.Strings(out)
395 return out
396}
397
398func (h *renewHeld) Err() error {
399 h.t.mu.Lock()
400 defer h.t.mu.Unlock()
401 if !h.t.held[h.key] {
402 return errors.New("renew table: released")
403 }
404 return nil
405}
406func (h *renewHeld) Lost() <-chan struct{} { return h.lost }
407func (h *renewHeld) Release(context.Context) error {
408 h.t.mu.Lock()
409 defer h.t.mu.Unlock()
410 delete(h.t.held, h.key)
411 return nil
412}
413
414// leases is the claim's park leases over the table, as Build assembles
415// them: RenewingLeases under AggregateLeases with the replica park lease set.
416func (r *renewTable) leases() hostbox.AggregateLeases {
417 return hostbox.AggregateLeases{Leases: hostbox.RenewingLeases{Region: "eu-west-1", Open: r.open}, Resolve: hostbox.ReplicaParkLeaseSet}
418}
419
420// ownLease is the build's own-location lease over the table, as
421// armPhase1's OwnLease takes it (RenewingLeases.Acquire).
422func (r *renewTable) ownLease(ctx context.Context, bucket, location string) (park.HeldLease, error) {
423 return hostbox.RenewingLeases{Region: "eu-west-1", Open: r.open}.Acquire(ctx, bucket, location)
424}
425
426// TestArmMovesPassRefusesAMismatchedBucketOrRegion (M17): the arms take the
427// claim's engine only when its bucket and region are this box's own; any
428// refusal sets none of the hooks.
429func TestArmMovesPassRefusesAMismatchedBucketOrRegion(t *testing.T) {
430 withMovesWakeSource(t, true)
431 view := func(context.Context, hostclaim.Observed) (park.View, error) { return park.View{}, nil }
432 auth := func(context.Context, hostclaim.Observed, hostclaim.Obligation) error { return nil }
433 providers := func(bucket, region string) ClaimProviders {
434 return ClaimProviders{moveParks: &claimMoveParks{
435 local: claimMoveLocalAuthorizer(func(context.Context) error { return nil }, auth),
436 engine: park.Engine{Bucket: bucket, Region: region, Leases: (&renewTable{}).leases(), View: view, Authorize: auth},
437 leases: hostbox.ReplicaParkLeaseSet, check: func(context.Context) error { return nil }}}
438 }
439 unarmed := func(t *testing.T, c moveCfg) {
440 t.Helper()
441 if c.ParkLeases != nil || c.ParkView != nil || c.ParkAuthorize != nil || c.ParkRunHeld != nil || c.ParkLocalAuthorize != nil {
442 t.Fatal("a refused arming set a hook")
443 }
444 }
445 for name, p := range map[string]ClaimProviders{"another bucket": providers("data-uk", "ie"), "another region": providers("data-ie", "uk")} {
446 t.Run(name, func(t *testing.T) {
447 c := moveCfg{OwnBucket: "data-ie", Region: "ie"}
448 if err := armMovesParks(t.Context(), &c, p); !errors.Is(err, hostbox.ErrChanged) {
449 t.Fatalf("armed: %v", err)
450 }
451 unarmed(t, c)
452 })
453 }
454 t.Run("no MoveParks", func(t *testing.T) {
455 c := moveCfg{OwnBucket: "data-ie", Region: "ie"}
456 if err := armMovesParks(t.Context(), &c, ClaimProviders{}); !errors.Is(err, hostwrite.ErrUnavailable) {
457 t.Fatalf("armed: %v", err)
458 }
459 unarmed(t, c)
460 })
461 t.Run("this box's own engine", func(t *testing.T) {
462 c := moveCfg{OwnBucket: "data-ie", Region: "ie"}
463 if err := armMovesParks(t.Context(), &c, providers("data-ie", "ie")); err != nil {
464 t.Fatal(err)
465 }
466 if c.ParkLeases == nil || c.ParkView == nil || c.ParkAuthorize == nil || c.ParkRunHeld == nil || c.ParkLocalAuthorize == nil {
467 t.Fatalf("hooks unset: %+v", c)
468 }
469 })
470}
471
472// TestArmMovesParksTakesRunHeldAndTheLocalAuthorizerFromTheClaim (M5, M9):
473// the arm's RunHeld is park.Engine.RunHeld over the claim's own leases, so a
474// residue park borrows the build's own-location lease through the claim's
475// AggregateLeases (AcquireHeld, LeaseScope) where Engine.Run would wait on
476// it; and its local authorizer is the claim's MoveParks one, which asks the
477// claim's identity check and owner and refuses anything but a park-local. A
478// claim engine whose leases cannot lend a held lease does not arm.
479func TestArmMovesParksTakesRunHeldAndTheLocalAuthorizerFromTheClaim(t *testing.T) {
480 withMovesWakeSource(t, true)
481 ctx := t.Context()
482 inc := strings.Repeat("a", 64)
483 loc := "instances/" + vwHost + "/" + inc
484 now := time.Date(2026, 10, 12, 12, 0, 0, 0, time.UTC)
485 claims := release.NewDirStore(t.TempDir())
486 if _, err := hostclaim.Update(ctx, claims, hostclaim.Observed{Host: vwHost, Absent: true}, func(c *hostclaim.Claim) error {
487 c.Op = hostclaim.Op{Kind: hostclaim.KindParking, ID: "op", Intent: map[string]string{"park_sweep": "v1"}}
488 c.Obligations = []hostclaim.Obligation{{ID: "chore", Op: "op", Kind: "park-replica", Region: "ie", Owner: "region:ie", Retirement: "ret",
489 Park: &hostclaim.ParkScope{Location: loc, Incarnation: inc, EvidenceAt: now.Add(-time.Hour), RequestLimit: hostbox.RequestLimit}}}
490 return nil
491 }); err != nil {
492 t.Fatal(err)
493 }
494 data := leasetest.NewVersionedBucket()
495 data.AddBucket("data-ie")
496 data.Put("data-ie", loc+"/db", []byte("residue"))
497 table := &renewTable{}
498 view := func(_ context.Context, o hostclaim.Observed) (park.View, error) {
499 return park.View{ClaimETag: o.ETag, Region: "ie", AuthoritativeRegion: movesNoLiveRegion}, nil
500 }
501 authorized := 0
502 auth := func(context.Context, hostclaim.Observed, hostclaim.Obligation) error { authorized++; return nil }
503 owned, identity := 0, error(nil)
504 owner := func(context.Context, hostclaim.Observed, hostclaim.Obligation) error { owned++; return nil }
505 providers := func(leases park.Leases) ClaimProviders {
506 return ClaimProviders{moveParks: &claimMoveParks{
507 local: claimMoveLocalAuthorizer(func(context.Context) error { return identity }, owner),
508 engine: park.Engine{Bucket: "data-ie", Region: "ie", Leases: leases, View: view, Authorize: auth},
509 leases: hostbox.ReplicaParkLeaseSet, check: func(context.Context) error { return nil }}}
510 }
511
512 t.Run("leases that cannot lend a held lease", func(t *testing.T) {
513 c := moveCfg{OwnBucket: "data-ie", Region: "ie"}
514 if err := armMovesParks(ctx, &c, providers(&fakeParkLeases{})); !errors.Is(err, hostwrite.ErrUnavailable) {
515 t.Fatalf("armed over leases with no AcquireHeld: %v", err)
516 }
517 if parkHooksArmed(c) || c.ParkRunHeld != nil || c.ParkLocalAuthorize != nil {
518 t.Fatal("a refused arming set a hook")
519 }
520 })
521
522 c := moveCfg{OwnBucket: "data-ie", Region: "ie"}
523 if err := armMovesParks(ctx, &c, providers(table.leases())); err != nil {
524 t.Fatal(err)
525 }
526 own, err := table.ownLease(ctx, c.OwnBucket, loc)
527 if err != nil {
528 t.Fatal(err)
529 }
530 t.Cleanup(own.Close)
531 // The engine as an install builds it (installRun.engine): this box's
532 // store and bucket, the armed leases, view and authorizer.
533 eng := park.Engine{Store: data, Claims: claims, Bucket: c.OwnBucket, Region: c.parkRegion(), Leases: c.ParkLeases,
534 Now: func() time.Time { return now }, NewID: func() string { return "next" }, View: c.ParkView, Authorize: c.ParkAuthorize}
535 w := park.Work{Host: vwHost, ID: "chore", Op: "op"}
536 if err := eng.Run(ctx, w); err == nil {
537 t.Fatal("Engine.Run took the location's lock while the build holds it")
538 }
539 if err := c.ParkRunHeld(ctx, eng, w, own); err != nil && !errors.Is(err, park.ErrPending) {
540 t.Fatal(err)
541 }
542 if versions, err := data.ListVersions(ctx, "data-ie", loc+"/"); err != nil || len(versions) != 0 {
543 t.Fatalf("the residue is still there: %v %v", versions, err)
544 }
545 if len(table.opens) != 1 || authorized == 0 {
546 t.Fatalf("lease opens %v, authorized %d: the park did not borrow the build's lease under the claim's authorizer", table.opens, authorized)
547 }
548 if err := own.Check(ctx); err != nil {
549 t.Fatalf("the run released the build's own lease: %v", err)
550 }
551
552 local := hostclaim.Obligation{ID: "local", Op: "op", Kind: "park-local", Box: "ie-box"}
553 if err := c.ParkLocalAuthorize(ctx, &hostclaim.Claim{Host: vwHost}, local); err != nil || owned != 1 {
554 t.Fatalf("the claim's owner was not asked: %v (%d)", err, owned)
555 }
556 if err := c.ParkLocalAuthorize(ctx, &hostclaim.Claim{Host: vwHost}, hostclaim.Obligation{ID: "chore", Op: "op", Kind: "park-replica"}); err == nil || owned != 1 {
557 t.Fatalf("a park-replica passed the local authorizer: %v", err)
558 }
559 identity = hostbox.ErrChanged
560 if err := c.ParkLocalAuthorize(ctx, &hostclaim.Claim{Host: vwHost}, local); !errors.Is(err, hostbox.ErrChanged) || owned != 1 {
561 t.Fatalf("the local authorizer skipped the claim's identity check: %v", err)
562 }
563}
564
565// TestMovesParksRefuseUntilTheWakeSourceRegisters (Q1): one gate,
566// movesWakeSourceRegistered, stands between the moves parks and production.
567// Until this box's moves source is registered with the claim
568// (registerMovesWakeSource, RegisterProductionClaimMoveSources), the
569// registered view refuses with a named dependency, reading nothing, so the
570// claim's sweep queues and runs nothing through it, and armMovesParks sets no
571// hook. Registering the park sources alone, or another source, does not
572// open it. Task 5's evidence is registered as it is, reachable as the
573// sweep verifier.
574func TestMovesParksRefuseUntilTheWakeSourceRegisters(t *testing.T) {
575 const contract = "moves wake owners and C8 files manifest provider (Plan 5 Task 9b)"
576 f := newVwFixture(t)
577 evidence := movesRetirementEvidence{Records: f.records(), Now: time.Now}
578 obs := vwObs(&hostclaim.Claim{Holder: f.holder("cur", "cur@ie"), Op: hostclaim.Op{Kind: hostclaim.KindServing, Exec: "box"}})
579 auth := func(context.Context, hostclaim.Observed, hostclaim.Obligation) error { return nil }
580 providers := ClaimProviders{moveParks: &claimMoveParks{
581 local: claimMoveLocalAuthorizer(func(context.Context) error { return nil }, auth),
582 engine: park.Engine{Bucket: "data", Region: "ie", Leases: (&renewTable{}).leases(), View: f.view("ie"), Authorize: auth},
583 leases: hostbox.ReplicaParkLeaseSet, check: func(context.Context) error { return nil }}}
584 shut := func(t *testing.T, src movesParkSources) {
585 t.Helper()
586 var dep *ClaimDependencyError
587 if _, err := src.View(t.Context(), obs); !errors.As(err, &dep) || !strings.Contains(err.Error(), contract) {
588 t.Fatalf("the view answered before the wake source registered: %v", err)
589 }
590 if f.reads != 0 || f.lists != 0 {
591 t.Fatal("a refused view read the record or the bucket")
592 }
593 c := moveCfg{OwnBucket: "data", Region: "ie"}
594 if err := armMovesParks(t.Context(), &c, providers); !errors.As(err, &dep) || !strings.Contains(err.Error(), contract) {
595 t.Fatalf("armed before the wake source registered: %v", err)
596 }
597 if parkHooksArmed(c) || c.ParkRunHeld != nil || c.ParkLocalAuthorize != nil {
598 t.Fatal("a refused arming set a hook")
599 }
600 }
601
602 t.Run("the park sources alone", func(t *testing.T) {
603 withMovesWakeSource(t, false)
604 src, err := newMovesParkSources(f.records(), f.bucket, "data", fixedRegion("ie"), evidence)
605 if err != nil {
606 t.Fatal(err)
607 }
608 if _, ok := src.Evidence.(park.MoveSweepVerifier); !ok {
609 t.Fatal("the registered evidence is not the sweep verifier")
610 }
611 shut(t, src)
612 if err := registerMovesParks(src); err != nil {
613 t.Fatal(err)
614 }
615 if p := claimMoveSources.Parks; p == nil || p.View == nil || p.Evidence == nil {
616 t.Fatalf("registered %+v", p)
617 }
618 if err := registerMovesParks(src); !errors.Is(err, hostwrite.ErrUnavailable) {
619 t.Fatalf("a second registration: %v", err)
620 }
621 shut(t, src)
622 if _, err := claimMoveSources.Parks.View(t.Context(), obs); err == nil {
623 t.Fatal("the claim's registered park view answered before the wake source registered")
624 }
625 })
626
627 t.Run("another source registered with the claim", func(t *testing.T) {
628 withMovesWakeSource(t, false)
629 src, err := newMovesParkSources(f.records(), f.bucket, "data", fixedRegion("ie"), evidence)
630 if err != nil {
631 t.Fatal(err)
632 }
633 if err := RegisterProductionClaimMoveSources(&parkRegistrationWake{}); err != nil {
634 t.Fatal(err)
635 }
636 shut(t, src)
637 if err := registerMovesWakeSource(&parkRegistrationWake{}); !errors.Is(err, hostwrite.ErrUnavailable) {
638 t.Fatalf("registered over another source: %v", err)
639 }
640 shut(t, src)
641 })
642
643 t.Run("this box's moves source", func(t *testing.T) {
644 withMovesWakeSource(t, false)
645 src, err := newMovesParkSources(f.records(), f.bucket, "data", fixedRegion("ie"), evidence)
646 if err != nil {
647 t.Fatal(err)
648 }
649 if err := registerMovesParks(src); err != nil {
650 t.Fatal(err)
651 }
652 if err := registerMovesWakeSource(nil); !errors.Is(err, hostwrite.ErrUnavailable) {
653 t.Fatalf("a nil source: %v", err)
654 }
655 shut(t, src)
656 if err := registerMovesWakeSource(&parkRegistrationWake{}); err != nil {
657 t.Fatal(err)
658 }
659 if err := movesWakeSourceRegistered(); err != nil {
660 t.Fatalf("the gate: %v", err)
661 }
662 if v, err := src.View(t.Context(), obs); err != nil || v.AuthoritativeRegion != "ie" {
663 t.Fatalf("view %+v, %v", v, err)
664 }
665 if v, err := claimMoveSources.Parks.View(t.Context(), obs); err != nil || v.AuthoritativeRegion != "ie" {
666 t.Fatalf("the claim's park view is not the moves view: %+v, %v", v, err)
667 }
668 c := moveCfg{OwnBucket: "data", Region: "ie"}
669 if err := armMovesParks(t.Context(), &c, providers); err != nil || !parkHooksArmed(c) {
670 t.Fatalf("not armed: %v", err)
671 }
672 if err := registerMovesWakeSource(&parkRegistrationWake{}); !errors.Is(err, hostwrite.ErrUnavailable) {
673 t.Fatalf("a second registration: %v", err)
674 }
675 if err := movesWakeSourceRegistered(); err != nil {
676 t.Fatalf("a refused second registration shut the gate: %v", err)
677 }
678 })
679}
680
681// TestCommittedLaneViewNamesTheTargetWithItsMarker (the Task 3 carry): after
682// the commit the target's lane park runs under the real view, which names
683// the target's region authoritative and supplies the marker the target's
684// probe published, read by its exact version; the lane is parked.
685func TestCommittedLaneViewNamesTheTargetWithItsMarker(t *testing.T) {
686 t.Parallel()
687 h := newCloseHarness(t)
688 ctx := context.Background()
689 location := strings.TrimSuffix(h.claim(t).Holder.Location, "/") // the cut's holder, at the bare location
690 body, err := json.Marshal(map[string]any{"op": p1ID, "incarnation": h.inc, "host": p1Host, "box": "uk-box", "installed_at": h.clock.Now(),
691 "manifest_sha256": "x", "snapshot_txid": 1})
692 if err != nil {
693 t.Fatal(err)
694 }
695 markerVersion, err := h.vs.PutIfAbsent(ctx, "data-uk", location+"/generation.json", body)
696 if err != nil {
697 t.Fatal(err)
698 }
699 real := movesParkView(MovesRecordsFrom(h.s), h.vs, "data-uk", "uk")
700 var committed []park.View
701 h.parkView = func(ctx context.Context, obs hostclaim.Observed) (park.View, error) {
702 v, err := real(ctx, obs)
703 if err == nil && obs.Claim.Op.Kind == hostclaim.KindLanding && obs.Claim.Op.Phase == hostclaim.PhaseCommitted {
704 committed = append(committed, v)
705 }
706 return v, err
707 }
708 h.land(t)
709 h.committedAndOpen(t)
710 if len(committed) == 0 {
711 t.Fatal("the committed lane park never asked the view")
712 }
713 for _, v := range committed {
714 if v.Region != "uk" || v.AuthoritativeRegion != "uk" || !v.MarkerKnown || v.MarkerGeneration != p1ID ||
715 v.Marker != (park.Ref{Key: location + "/generation.json", VersionID: markerVersion}) {
716 t.Fatalf("the committed view %+v; want uk authoritative with the marker at %s", v, markerVersion)
717 }
718 }
719 lane := moves.ScopeLocation(p1Host, p1ID) + "/"
720 for k := range h.f.buckets["data-uk"] {
721 if strings.HasPrefix(k, lane) {
722 t.Fatalf("the lane still holds %s after the committed park", k)
723 }
724 }
725}
726
727// TestRegisterProductionMovesParksNeedsBothSwitches (ruling H1): with either
728// switch off the agent registers nothing. With both on it registers without
729// CARLOS_REGION: the view's region is the claim's verified roster row's
730// (fix round 1, region source), resolved when the view is first asked. What
731// it registers stays shut (Q1) until the moves wake source registers right
732// after it (registerProductionMovesWakeSource, Task 8): the parks alone
733// leave the claim's park view refusing every host before it reads anything,
734// so the claim's sweep is not widened.
735func TestRegisterProductionMovesParksNeedsBothSwitches(t *testing.T) {
736 withMovesWakeSource(t, false)
737 env := func(vals map[string]string) func(string) string { return func(k string) string { return vals[k] } }
738 hib := activator.Config{Bucket: "data-ie", Region: "eu-west-1"}
739 store := release.NewDirStore(t.TempDir())
740 asked := 0
741 roster := func(context.Context) (string, error) { asked++; return "ie", nil }
742 for name, vals := range map[string]map[string]string{
743 "claims off": {moves.SwitchEnv: "on"},
744 "moves off": {"CARLOS_HOST_CLAIM": "1", moves.SwitchEnv: "off"},
745 } {
746 if _, err := registerProductionMovesParks(t.Context(), env(vals), store, hib, roster, newMovesRefusals("ie-box", nil, t.Logf, time.Now)); err != nil {
747 t.Fatalf("%s: %v", name, err)
748 }
749 if claimMoveSources.Parks != nil {
750 t.Fatalf("%s: registered", name)
751 }
752 }
753 if _, err := registerProductionMovesParks(t.Context(), env(map[string]string{"CARLOS_HOST_CLAIM": "1", moves.SwitchEnv: "on"}), store, hib, roster, newMovesRefusals("ie-box", nil, t.Logf, time.Now)); err != nil {
754 t.Fatalf("both switches on, no CARLOS_REGION: %v", err)
755 }
756 if claimMoveSources.Parks == nil || asked != 0 {
757 t.Fatalf("registered %v, roster asked %d times at registration", claimMoveSources.Parks != nil, asked)
758 }
759 f := newVwFixture(t)
760 obs := vwObs(&hostclaim.Claim{Holder: f.holder("cur", "cur@ie"), Op: hostclaim.Op{Kind: hostclaim.KindServing, Exec: "box"}})
761 var dep *ClaimDependencyError
762 if _, err := claimMoveSources.Parks.View(t.Context(), obs); !errors.As(err, &dep) || asked != 0 || claimMoveSources.Source != nil {
763 t.Fatalf("the production park view answered (%v), roster asked %d times", err, asked)
764 }
765}
766
767// TestHomeBoxArms (fix round 1, region source; home-region parks, part 1;
768// claim Plan 13): a home box, whose placement code is "", is seen from
769// park.HomeRegion, and its arms take the claim's engine only when the claim
770// grants it the @home alias: the engine is named by the box's signed roster
771// region and carries that region as its HomeRegion (the fleet's signed home
772// designation). Without the grant, or with a home that is another region, a
773// home box stays unarmed, even over an engine that names itself @home. An
774// engine region of "" (unset) never arms, and neither does a box whose code
775// is no placement, even when the engine's region spells the same string. A
776// named box arms over its own named engine whatever the fleet's home is. The
777// gate is open here; in production it stays shut until Task 8.
778func TestHomeBoxArms(t *testing.T) {
779 withMovesWakeSource(t, true)
780 f := newVwFixture(t)
781 f.rec.Region = ""
782 view := movesParkViewAt(f.records(), f.bucket, "data", fixedRegion(park.HomeRegion))
783 obs := vwObs(&hostclaim.Claim{Holder: f.holder(""), Op: hostclaim.Op{Kind: hostclaim.KindMoving, ID: vwMove, Phase: hostclaim.PhaseMoving, Exec: "box"}})
784 f.rec.Move = &provision.MoveState{ID: vwMove, Incarnation: f.inc, From: "", To: "uk", State: provision.MoveCutting}
785 v, err := view(t.Context(), obs)
786 if err != nil || v.Region != park.HomeRegion || v.AuthoritativeRegion != park.HomeRegion || !v.MarkerKnown {
787 t.Fatalf("the home view %+v, %v", v, err)
788 }
789 auth := func(context.Context, hostclaim.Observed, hostclaim.Obligation) error { return nil }
790 arm := func(engineRegion, home, placement string) (moveCfg, error) {
791 providers := ClaimProviders{moveParks: &claimMoveParks{
792 local: claimMoveLocalAuthorizer(func(context.Context) error { return nil }, auth),
793 engine: park.Engine{Bucket: "data-home", Region: engineRegion, HomeRegion: home, Leases: (&renewTable{}).leases(), View: view, Authorize: auth},
794 leases: hostbox.ReplicaParkLeaseSet, check: func(context.Context) error { return nil }}}
795 c := moveCfg{OwnBucket: "data-home", Region: placement}
796 err := armMovesParks(t.Context(), &c, providers)
797 return c, err
798 }
799 c, err := arm("ie", "ie", "")
800 if err != nil {
801 t.Fatalf("a home box the claim grants @home did not arm: %v", err)
802 }
803 if !parkHooksArmed(c) || c.ParkHomeRegion != "ie" {
804 t.Fatalf("hooks unset or the home binding lost: %q", c.ParkHomeRegion)
805 }
806 for _, tc := range [][3]string{{"uk", "", "uk"}, {"uk", "ie", "uk"}} {
807 if c, err := arm(tc[0], tc[1], tc[2]); err != nil || !parkHooksArmed(c) {
808 t.Fatalf("a named box (engine %q, home %q) did not arm: %v", tc[0], tc[1], err)
809 }
810 }
811 for _, tc := range [][3]string{
812 {"ie", "", ""}, // no alias granted
813 {"ie", "uk", ""}, // the fleet's home is another region
814 {park.HomeRegion, "", ""}, // an engine naming itself @home, no grant
815 {"", "", ""}, {"", "ie", ""}, // an unset engine region
816 {"home", "", ""}, // the roster's own word, with no grant
817 {park.HomeRegion, "", "home"}, {park.HomeRegion, "", park.HomeRegion}, {"IE", "", "IE"}, {"", "", "uk"},
818 } {
819 if c, err := arm(tc[0], tc[1], tc[2]); !errors.Is(err, hostbox.ErrChanged) || parkHooksArmed(c) {
820 t.Fatalf("engine in %q, home %q, placement %q: armed (%v)", tc[0], tc[1], tc[2], err)
821 }
822 }
823 if provision.ValidRegion(movesNoLiveRegion) || movesNoLiveRegion == park.HomeRegion {
824 t.Fatalf("the no-live sentinel %q can be a box's region", movesNoLiveRegion)
825 }
826}
827
828// TestParkViewRegionIsTheRosters (fix round 1, region source): the view
829// asks the roster resolver once, caches a success, and a failure refuses
830// that call and asks again on the next.
831func TestParkViewRegionIsTheRosters(t *testing.T) {
832 f := newVwFixture(t)
833 calls, fail := 0, true
834 resolve := cachedRegion(func(context.Context) (string, error) {
835 calls++
836 if fail {
837 return "", hostbox.ErrChanged
838 }
839 return "uk", nil
840 })
841 view := movesParkViewAt(f.records(), f.bucket, "data", resolve)
842 obs := vwObs(&hostclaim.Claim{Holder: f.holder("cur", "cur@ie"), Op: hostclaim.Op{Kind: hostclaim.KindServing, Exec: "box"}})
843 if _, err := view(t.Context(), obs); !errors.Is(err, hostbox.ErrChanged) {
844 t.Fatalf("a roster failure: %v", err)
845 }
846 fail = false
847 for i := 0; i < 3; i++ {
848 if v, err := view(t.Context(), obs); err != nil || v.Region != "uk" {
849 t.Fatalf("view %+v, %v", v, err)
850 }
851 }
852 if calls != 2 {
853 t.Fatalf("the roster was asked %d times, want 2", calls)
854 }
855}
856
857// TestLandingResidueParkUnderItsOwnLease (M5, M6): the landing takes its
858// lease on the location at checking and holds it through published (spec
859// §7.6). The leases are the claim's own shape over a table behind
860// RenewingLeases' Open: the landing's own lease is a RenewingLeases one (as
861// armPhase1's OwnLease is) and the park leases are AggregateLeases over the
862// replica park lease set (as the claim's MoveParks engine's are). The
863// residue park's scope resolves to the landing's own lock, so Engine.Run
864// would wait on the landing itself; park.Engine.RunHeld borrows the held
865// lease through AcquireHeld and LeaseScope, and the landing goes on to
866// restored.
867func TestLandingResidueParkUnderItsOwnLease(t *testing.T) {
868 t.Parallel()
869 location := "data-uk/" + cutRoot
870 t.Run("under RunHeld", func(t *testing.T) {
871 h := newLandingHarness(t)
872 h.flip(t)
873 keys := h.residue(t)
874 table := &renewTable{}
875 lender := &countingLender{AggregateLeases: table.leases()}
876 h.ownLease, h.parkLeases = table.ownLease, lender
877 h.land(t)
878 h.installed(t)
879 if lender.held == 0 {
880 t.Fatal("no residue park borrowed the landing's lease through the claim's leases")
881 }
882 for _, o := range table.opens {
883 if o != location {
884 t.Fatalf("a lease other than the landing's own was opened: %v", table.opens)
885 }
886 }
887 for _, k := range keys {
888 if _, ok := h.f.buckets["data-uk"][k]; ok && !strings.HasSuffix(k, "/generation.json") {
889 t.Fatalf("%s is still at the location", k)
890 }
891 }
892 })
893 t.Run("Engine.Run waits on the landing's own lease", func(t *testing.T) {
894 h := newLandingHarness(t)
895 h.flip(t)
896 keys := h.residue(t)
897 table := &renewTable{}
898 h.ownLease, h.parkLeases = table.ownLease, table.leases()
899 h.runHeld = func(ctx context.Context, e park.Engine, w park.Work, _ park.HeldLease) error { return e.Run(ctx, w) }
900 for i := 0; i < 3; i++ {
901 h.land(t)
902 }
903 if m := h.read(t, p1Host).Move; m.LandingPhase != provision.LandParking {
904 t.Fatalf("the landing went past parking without RunHeld: %+v", m)
905 }
906 if got := table.holding(); len(got) != 1 || got[0] != location {
907 t.Fatalf("the landing holds %v", got)
908 }
909 for _, k := range keys {
910 if _, ok := h.f.buckets["data-uk"][k]; !ok {
911 t.Fatalf("%s was parked under a lease nobody held", k)
912 }
913 }
914 })
915}
916
917// countingLender is the claim's AggregateLeases, counting the held leases
918// it lends (AcquireHeld).
919type countingLender struct {
920 hostbox.AggregateLeases
921 held int
922}
923
924func (l *countingLender) AcquireHeld(ctx context.Context, bucket, scope string, own park.HeldLease) (park.HeldLease, error) {
925 l.held++
926 return l.AggregateLeases.AcquireHeld(ctx, bucket, scope, own)
927}
928
929// TestNoArmRunsWithoutTheThreeParkHooks (M17): every moves arm requires the
930// claim's park leases, view and authorizer, the landing (and the recovery's
931// install) RunHeld too, the finalization the claim's local authorizer;
932// production's own wiring sets none of them until the wake-source gate
933// opens.
934func TestNoArmRunsWithoutTheThreeParkHooks(t *testing.T) {
935 t.Parallel()
936 three := map[string]func(*moveCfg){
937 "no park leases": func(c *moveCfg) { c.ParkLeases = nil },
938 "no park view": func(c *moveCfg) { c.ParkView = nil },
939 "no park authorizer": func(c *moveCfg) { c.ParkAuthorize = nil },
940 }
941 runHeld := func(c *moveCfg) { c.ParkRunHeld = nil }
942 local := func(c *moveCfg) { c.ParkLocalAuthorize = nil }
943 arms := []struct {
944 name string
945 cfg func(t *testing.T) moveCfg
946 armed func(moveCfg) bool
947 extra map[string]func(*moveCfg)
948 }{
949 {"landing", func(t *testing.T) moveCfg { return newLandingHarness(t).lcfg }, landingArmed, map[string]func(*moveCfg){"no RunHeld": runHeld}},
950 {"rollback target", func(t *testing.T) moveCfg { h := newRollbackHarness(t); return h.rollbackCfg(t) }, rollbackTargetArmed, nil},
951 {"lane park (revert, abort, committed)", func(t *testing.T) moveCfg { return newRevertHarness(t).laneCfg() }, revertLaneArmed, nil},
952 {"recovery", func(t *testing.T) moveCfg { h := newRecoverHarness(t, true); return h.sourceCfg(t) }, recoverySourceArmed, map[string]func(*moveCfg){"no RunHeld": runHeld}},
953 {"source finalization", func(t *testing.T) moveCfg { return newFinalHarness(t).cfg }, sourceFinalArmed, map[string]func(*moveCfg){"no claim local authorizer": local}},
954 }
955 for _, a := range arms {
956 t.Run(a.name, func(t *testing.T) {
957 c := a.cfg(t)
958 if !a.armed(c) {
959 t.Fatal("fixture: the arm is not armed with every hook")
960 }
961 for name, unarm := range three {
962 u := c
963 unarm(&u)
964 if a.armed(u) {
965 t.Fatalf("%s: armed", name)
966 }
967 }
968 for name, unarm := range a.extra {
969 u := c
970 unarm(&u)
971 if a.armed(u) {
972 t.Fatalf("%s: armed", name)
973 }
974 }
975 })
976 }
977}
978
979// TestNoArmRunsParkLocalWithoutTheClaimsLocalAuthorizer (M9, Q10): every arm
980// that runs a park.Local (the landing's install, the target's half of a
981// rollback, the source's recovery through the install, the source's
982// finalization) requires the claim's MoveParks local authorizer, and the
983// park.Local authorizer an install builds asks its own check and then the
984// claim's: there is no fallback to its own rule alone.
985func TestNoArmRunsParkLocalWithoutTheClaimsLocalAuthorizer(t *testing.T) {
986 t.Parallel()
987 arms := []struct {
988 name string
989 cfg func(t *testing.T) moveCfg
990 armed func(moveCfg) bool
991 }{
992 {"landing", func(t *testing.T) moveCfg { return newLandingHarness(t).lcfg }, landingArmed},
993 {"rollback target", func(t *testing.T) moveCfg { h := newRollbackHarness(t); return h.rollbackCfg(t) }, rollbackTargetArmed},
994 {"recovery", func(t *testing.T) moveCfg { h := newRecoverHarness(t, true); return h.sourceCfg(t) }, recoverySourceArmed},
995 {"source finalization", func(t *testing.T) moveCfg { return newFinalHarness(t).cfg }, sourceFinalArmed},
996 }
997 for _, a := range arms {
998 t.Run(a.name, func(t *testing.T) {
999 c := a.cfg(t)
1000 if !a.armed(c) {
1001 t.Fatal("fixture: the arm is not armed with every hook")
1002 }
1003 c.ParkLocalAuthorize = nil
1004 if a.armed(c) {
1005 t.Fatal("armed without the claim's local authorizer")
1006 }
1007 })
1008 }
1009 t.Run("the install's park.Local authorizer", func(t *testing.T) {
1010 h := newLandingHarness(t)
1011 ctx := t.Context()
1012 build := func(local func(context.Context, *hostclaim.Claim, hostclaim.Obligation) error) *installRun {
1013 c := h.lcfg
1014 c.ParkLocalAuthorize = local
1015 rec := provision.Record{Host: p1Host, Move: &provision.MoveState{ID: p1ID, Incarnation: h.inc}}
1016 return newInstallRun(h.s, &movesWorker{cfg: c}, rec, installSource{OpID: p1ID, Host: p1Host, Role: roleTarget})
1017 }
1018 box := h.lcfg.Box
1019 ours := &hostclaim.Claim{Host: p1Host, Op: hostclaim.Op{ID: p1ID, Kind: hostclaim.KindLanding, Exec: box}}
1020 chore := hostclaim.Obligation{ID: "local", Op: p1ID, Kind: "park-local", Box: box}
1021 if err := build(nil).local.Authorize(ctx, ours, chore); err == nil {
1022 t.Fatal("the install's own rule stood alone without the claim's local authorizer")
1023 }
1024 asked := 0
1025 claim := func(context.Context, *hostclaim.Claim, hostclaim.Obligation) error { asked++; return nil }
1026 if err := build(claim).local.Authorize(ctx, ours, chore); err != nil || asked != 1 {
1027 t.Fatalf("the claim's local authorizer was not asked: %v (%d)", err, asked)
1028 }
1029 other := &hostclaim.Claim{Host: p1Host, Op: hostclaim.Op{ID: "mv_other", Kind: hostclaim.KindLanding, Exec: box}}
1030 if err := build(claim).local.Authorize(ctx, other, chore); err == nil || asked != 1 {
1031 t.Fatalf("the install's own check did not run first: %v (%d)", err, asked)
1032 }
1033 })
1034}
1035