| 1 | package host |
| 2 | |
| 3 | import ( |
| 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 | |
| 29 | const ( |
| 30 | vwHost = "vw.example" |
| 31 | vwAcct = "acct" |
| 32 | vwApp = "app" |
| 33 | vwMove = "mv_vwvwvwvwvwvwvwvwvwvwvwvwvw" |
| 34 | vwRb = "rb_vwvwvwvwvwvwvwvwvwvwvwvwvw" |
| 35 | ) |
| 36 | |
| 37 | var 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. |
| 41 | type vwFixture struct { |
| 42 | inc, location string |
| 43 | rec *provision.Record |
| 44 | bucket *leasetest.VersionedBucket |
| 45 | reads, lists int |
| 46 | } |
| 47 | |
| 48 | func 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 | |
| 60 | func (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. |
| 73 | type countingReader struct { |
| 74 | *leasetest.VersionedBucket |
| 75 | f *vwFixture |
| 76 | } |
| 77 | |
| 78 | func (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 | |
| 83 | func (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". |
| 89 | func (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 | |
| 99 | func vwObs(c *hostclaim.Claim) hostclaim.Observed { |
| 100 | c.Host = vwHost |
| 101 | return hostclaim.Observed{Host: vwHost, ETag: "etag-1", Claim: c} |
| 102 | } |
| 103 | |
| 104 | func (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. |
| 110 | func (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. |
| 125 | func 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. |
| 243 | func 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. |
| 292 | func 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). |
| 329 | func 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. |
| 358 | type renewTable struct { |
| 359 | mu sync.Mutex |
| 360 | held map[string]bool |
| 361 | opens []string |
| 362 | } |
| 363 | |
| 364 | type renewHeld struct { |
| 365 | t *renewTable |
| 366 | key string |
| 367 | lost chan struct{} |
| 368 | } |
| 369 | |
| 370 | func (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 | |
| 385 | func (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 | |
| 398 | func (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 | } |
| 406 | func (h *renewHeld) Lost() <-chan struct{} { return h.lost } |
| 407 | func (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. |
| 416 | func (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). |
| 422 | func (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. |
| 429 | func 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. |
| 479 | func 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. |
| 574 | func 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. |
| 685 | func 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. |
| 735 | func 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. |
| 778 | func 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. |
| 831 | func 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. |
| 867 | func 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). |
| 919 | type countingLender struct { |
| 920 | hostbox.AggregateLeases |
| 921 | held int |
| 922 | } |
| 923 | |
| 924 | func (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. |
| 934 | func 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. |
| 985 | func 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 | |