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 "database/sql"
6 "encoding/json"
7 "errors"
8 "fmt"
9 "io"
10 "io/fs"
11 "log/slog"
12 "os"
13 "path/filepath"
14 "slices"
15 "sort"
16 "strings"
17 "sync"
18 "testing"
19 "time"
20
21 "github.com/benbjohnson/litestream"
22 lsfile "github.com/benbjohnson/litestream/file"
23 "github.com/carlosframework/platform/internal/activator"
24 "github.com/carlosframework/platform/internal/appconfig"
25 "github.com/carlosframework/platform/internal/dbverify"
26 "github.com/carlosframework/platform/internal/hostbox"
27 "github.com/carlosframework/platform/internal/hostclaim"
28 "github.com/carlosframework/platform/internal/hostfiles"
29 "github.com/carlosframework/platform/internal/hostguard"
30 "github.com/carlosframework/platform/internal/hostwrite"
31 "github.com/carlosframework/platform/internal/lease"
32 "github.com/carlosframework/platform/internal/moves"
33 "github.com/carlosframework/platform/internal/moves/archive"
34 "github.com/carlosframework/platform/internal/park"
35 "github.com/carlosframework/platform/internal/provision"
36 "github.com/carlosframework/platform/internal/registry"
37 "github.com/carlosframework/platform/internal/release"
38 "github.com/superfly/ltx"
39)
40
41// The landing harness: the cut harness with a REAL litestream replica in
42// the source's lineage (so the target's build restores a real SQLite file
43// that passes its check), driven to the flip, and a target box (uk-box)
44// with its own data directory, the held row reconcile would have minted,
45// the landing's hooks faked where main has no production provider yet
46// (ruling G17) and real where it has one: park.Local and hostguard.Guard
47// over the target's data directory, dbverify over the store, the hostbox
48// marker writer, moves.BuildGeneration over the fake versioned bucket.
49//
50// fakeParkLeases is one lease table for the landing's own-location lease
51// and the park engine's: a second acquisition of a held location is
52// refused. The landing holds its lease from checking through published
53// (spec §7.6), so its residue parks run under it through the claim's
54// AggregateLeases and park.Engine.RunHeld (M5); an Engine.Run that took the
55// lock again would fail the test.
56
57type landingHarness struct {
58 *cutHarness
59 // carry is the target's moveCfg.CarryRequest (nil: none), for the return
60 // lane's request carry (buildstate_carry_test.go).
61 carry func(ctx context.Context, host, bucket, prefix string) error
62 tdir string // the target's data directory
63 leases *fakeParkLeases
64 lw *movesWorker
65 lcfg moveCfg
66 route registry.Route
67 rows int // rows the source database holds
68 n uint64
69 replaced [][]registry.Timer
70 repl []string
71 nextDue []int64
72 renders int
73 render func() (appconfig.HeldRender, error)
74 // renderHook, when set, is the whole held render: it gets the
75 // before-write callback the installer passes (fix round 1, I1).
76 renderHook func(before func(string) error) (appconfig.HeldRender, error)
77 release moves.Check
78 landAt string
79 landHits []string
80 landDone bool
81 parkAt string
82 parkHits []string
83 parkDone bool
84 parkView func(context.Context, hostclaim.Observed) (park.View, error)
85 landFault func(point string) error
86 store lease.VersionedStore // when set, the target's versioned store
87
88 // renderPin, when set, sees the pins each render is asked to hold to
89 // (moves Task 7).
90 renderPin func(pin *provision.MoveInputs)
91 // releaseLive, when set, is the configured step's release
92 // (ReleaseLive, moves Task 7).
93 releaseLive func(ctx context.Context, s release.Store, rec provision.Record, version string) moves.Check
94
95 // probe, when set, is the target's probe wake (Plan 3 Task 6).
96 probe func(context.Context, activator.ProbeConfig) (activator.ProbeResult, error)
97 // probeLive, when set, is the activator's live probe entry.
98 probeLive func(string) (string, activator.ProbeResult, bool)
99 // openLog records the target's open (Plan 3 Task 7): the probe entry's
100 // mark and the hold's release, in order, with the record and claim
101 // state each saw.
102 openLog []string
103 // noRelease leaves ReleaseHeld unwired (the open unarmed).
104 noRelease bool
105 // holdProbed stops every pass at probed, before the commit (Task 6's
106 // tests, which end there).
107 holdProbed bool
108 // releaseNoop makes ReleaseHeld match nothing and change nothing.
109 releaseNoop bool
110 // releaseErr makes ReleaseHeld fail.
111 releaseErr error
112 // ownAuthorize drops the park authorizer the target would inherit from
113 // the source's configuration (the finalization harness sets one), so
114 // the target's parks use the allowing fake behind their own checks
115 // (Plan 5 Task 3).
116 ownAuthorize bool
117 // parkLeases, runHeld and ownLease, when set, replace the park fakes'
118 // leases, the claim's RunHeld (park.Engine.RunHeld, M5) and the
119 // landing's own-location lease.
120 parkLeases park.Leases
121 runHeld func(ctx context.Context, e park.Engine, w park.Work, held park.HeldLease) error
122 ownLease func(ctx context.Context, bucket, location string) (park.HeldLease, error)
123}
124
125// fakeParkLeases refuses a second acquisition of a held location.
126type fakeParkLeases struct {
127 mu sync.Mutex
128 held map[string]bool
129 leases map[string]*fakeHeldLease
130 acquired []string
131}
132
133// fakeHeldLease is a park.HeldLease with the Context and LeaseScope methods
134// hostbox's renewingHeld has: a context derived from it is cancelled when
135// the lease is lost or released, and the claim's AggregateLeases lends it to
136// RunHeld for its own bucket and scope.
137type fakeHeldLease struct {
138 l *fakeParkLeases
139 key string
140 bucket, scope string
141 ctx context.Context
142 cancel context.CancelFunc
143}
144
145// LeaseScope is renewingHeld.LeaseScope: the bucket and scope it holds.
146func (h *fakeHeldLease) LeaseScope() (string, string) { return h.bucket, h.scope }
147
148func (l *fakeParkLeases) Acquire(_ context.Context, bucket, scope string) (park.HeldLease, error) {
149 l.mu.Lock()
150 defer l.mu.Unlock()
151 key := bucket + "/" + scope
152 if l.held[key] {
153 return nil, &lease.HeldError{Owner: "the landing"}
154 }
155 if l.held == nil {
156 l.held = map[string]bool{}
157 }
158 l.held[key] = true
159 l.acquired = append(l.acquired, key)
160 ctx, cancel := context.WithCancel(context.Background())
161 h := &fakeHeldLease{l: l, key: key, bucket: bucket, scope: scope, ctx: ctx, cancel: cancel}
162 if l.leases == nil {
163 l.leases = map[string]*fakeHeldLease{}
164 }
165 l.leases[key] = h
166 return h, nil
167}
168
169// Context is renewingHeld.Context: ctx, cancelled too when the lease is.
170func (h *fakeHeldLease) Context(ctx context.Context) context.Context {
171 c, cancel := context.WithCancel(ctx)
172 stop := context.AfterFunc(h.ctx, cancel)
173 context.AfterFunc(c, func() { stop() })
174 return c
175}
176
177func (l *fakeParkLeases) holding() []string {
178 l.mu.Lock()
179 defer l.mu.Unlock()
180 var out []string
181 for k, on := range l.held {
182 if on {
183 out = append(out, k)
184 }
185 }
186 sort.Strings(out)
187 return out
188}
189
190func (h *fakeHeldLease) Check(context.Context) error {
191 h.l.mu.Lock()
192 defer h.l.mu.Unlock()
193 if !h.l.held[h.key] {
194 return errors.New("fake lease: released")
195 }
196 return nil
197}
198func (h *fakeHeldLease) Release(context.Context) error {
199 h.l.mu.Lock()
200 defer h.l.mu.Unlock()
201 delete(h.l.held, h.key)
202 h.cancel()
203 return nil
204}
205func (h *fakeHeldLease) Close() { _ = h.Release(context.Background()) }
206
207// realReplica writes a small SQLite database with rows rows over syncs
208// litestream syncs (a level-9 snapshot too) and returns the file replica
209// client and the newest level-0 position, as Task 8's test does.
210func realReplica(t *testing.T, syncs int) (litestream.ReplicaClient, uint64, int) {
211 t.Helper()
212 dir := t.TempDir()
213 path := filepath.Join(dir, "src.db")
214 rows := 0
215 write := func(n int) {
216 d, err := sql.Open("sqlite", path)
217 if err != nil {
218 t.Fatal(err)
219 }
220 defer d.Close()
221 if _, err := d.Exec(`CREATE TABLE IF NOT EXISTS t (id INTEGER PRIMARY KEY, body TEXT)`); err != nil {
222 t.Fatal(err)
223 }
224 for i := 0; i < n; i++ {
225 if _, err := d.Exec(`INSERT INTO t (body) VALUES (?)`, strings.Repeat("x", 100)); err != nil {
226 t.Fatal(err)
227 }
228 }
229 rows += n
230 }
231 write(1)
232 client := lsfile.NewReplicaClient(filepath.Join(dir, "replica"))
233 db := litestream.NewDB(path)
234 db.Logger = slog.New(slog.NewTextHandler(io.Discard, nil))
235 db.SetMetaPath(filepath.Join(dir, "meta"))
236 db.MonitorInterval = time.Hour
237 db.Replica = litestream.NewReplicaWithClient(db, client)
238 db.Replica.SyncInterval = time.Hour
239 if err := db.Open(); err != nil {
240 t.Fatal(err)
241 }
242 ctx := context.Background()
243 for i := 0; i < syncs; i++ {
244 if err := db.SyncAndWait(ctx); err != nil {
245 t.Fatal(err)
246 }
247 write(2)
248 }
249 if err := db.SyncAndWait(ctx); err != nil {
250 t.Fatal(err)
251 }
252 if _, err := db.Snapshot(ctx); err != nil {
253 t.Fatal(err)
254 }
255 if err := db.Close(ctx); err != nil {
256 t.Fatal(err)
257 }
258 var n uint64
259 for _, f := range replicaLevelFiles(t, client, 0) {
260 if uint64(f.MaxTXID) > n {
261 n = uint64(f.MaxTXID)
262 }
263 }
264 if n == 0 {
265 t.Fatal("the replica has no level-0 files")
266 }
267 return client, n, rows
268}
269
270func replicaLevelFiles(t *testing.T, client litestream.ReplicaClient, level int) []*ltx.FileInfo {
271 t.Helper()
272 itr, err := client.LTXFiles(context.Background(), level, 0, false)
273 if err != nil {
274 t.Fatal(err)
275 }
276 infos, err := ltx.SliceFileIterator(itr)
277 if err != nil {
278 t.Fatal(err)
279 }
280 return infos
281}
282
283func countRowsForTest(t *testing.T, path string) int {
284 t.Helper()
285 d, err := sql.Open("sqlite", "file:"+path+"?mode=ro")
286 if err != nil {
287 t.Fatal(err)
288 }
289 defer d.Close()
290 var n int
291 if err := d.QueryRow(`SELECT count(*) FROM t`).Scan(&n); err != nil {
292 t.Fatal(err)
293 }
294 return n
295}
296
297func newLandingHarness(t *testing.T, opts ...p1Option) *landingHarness {
298 t.Helper()
299 h := &landingHarness{cutHarness: newCutHarness(t, opts...), leases: &fakeParkLeases{}, release: moves.Check{OK: true}}
300 // The real replica replaces the harness's fake level files under the
301 // source's lineage; the final checkpoint adds nothing (the database is
302 // quiet at N).
303 client, n, rows := realReplica(t, 2)
304 h.n, h.rows = n, rows
305 loc := "instances/" + p1Host + "/" + h.inc + "/"
306 for k := range h.f.buckets[h.srcBucket] {
307 if strings.HasPrefix(k, loc+"g-cur/") && strings.HasSuffix(k, ".ltx") {
308 delete(h.f.buckets[h.srcBucket], k)
309 }
310 }
311 for level := 0; level <= litestream.SnapshotLevel; level++ {
312 for _, info := range replicaLevelFiles(t, client, level) {
313 rc, err := client.OpenLTXFile(context.Background(), info.Level, info.MinTXID, info.MaxTXID, 0, 0)
314 if err != nil {
315 t.Fatal(err)
316 }
317 body, err := io.ReadAll(rc)
318 rc.Close()
319 if err != nil {
320 t.Fatal(err)
321 }
322 key := loc + "g-cur/" + fmt.Sprintf("%04x/", level) + ltx.FormatFilename(info.MinTXID, info.MaxTXID)
323 h.f.store(h.srcBucket, key, fakeMoveObject{lease.MirrorObject{ETag: "r-" + filepath.Base(key), Size: int64(len(body))}, string(body)})
324 }
325 }
326 h.localPos = n
327 h.cfg.FinalCheckpoint = func(ctx context.Context, _, _ string, _ hostclaim.Holder, check func(context.Context) error) error {
328 return check(ctx)
329 }
330 // The target's data directory, with the two directories the guard and
331 // park.Local need, and the held row reconcile minted.
332 h.tdir = t.TempDir()
333 for _, d := range []string{"hostguard", ".parked"} {
334 if err := os.Mkdir(filepath.Join(h.tdir, d), 0o770); err != nil {
335 t.Fatal(err)
336 }
337 }
338 h.route = registry.Route{Host: p1Host, Kind: "instance", App: "app", Account: "bab", Hibernate: true, Status: "landing", HoldOp: p1ID,
339 Socket: provision.DefaultSocket(p1Host), DBPath: provision.DefaultDBPath(h.tdir, p1Host), Provisioned: true}
340 h.render = func() (appconfig.HeldRender, error) {
341 in := h.read(t, p1Host).Move.Inputs
342 if in == nil {
343 return appconfig.HeldRender{}, errors.New("no pinned inputs")
344 }
345 return appconfig.HeldRender{Path: filepath.Join(h.tdir, p1Host+".json"), SHA256: "x", BundleETags: in.BundleETags, OverlayETag: in.OverlayETag}, nil
346 }
347 // The restore-verification record names the host: local removes it.
348 if err := dbverify.Publish(context.Background(), h.s, &dbverify.Record{V: 1, Box: h.dstBox, CheckedAt: h.clock.Now(),
349 Hosts: map[string]dbverify.Verdict{p1Host: {OK: true, VerifiedAt: h.clock.Now()}, "other.example": {OK: true, VerifiedAt: h.clock.Now()}}}); err != nil {
350 t.Fatal(err)
351 }
352 h.lcfg = h.landingCfg(t)
353 return h
354}
355
356// landingCfg is the target's configuration with the landing armed.
357func (h *landingHarness) landingCfg(t *testing.T) moveCfg {
358 t.Helper()
359 c := h.target()
360 c.DataDir = h.tdir
361 c.CarryRequest = h.carry
362 c.OwnsRoute = func(host string) bool { return host == p1Host }
363 c.Route = func(host string) (*registry.Route, error) {
364 if host != p1Host {
365 return nil, nil
366 }
367 rt := h.route
368 return &rt, nil
369 }
370 c.FilesDir = func(host string) (string, string) {
371 return filepath.Join(h.tdir, host), filepath.Join(h.tdir, host+".env")
372 }
373 c.Guard = hostguard.Guard{DataDir: h.tdir, LockDir: t.TempDir()}
374 c.Local = park.Local{DataDir: h.tdir, Box: h.dstBox}
375 inherited := c.ParkAuthorize
376 armParkFakes(&c, h.s, h.leases, park.PlacementRegion(c.Region), roleTarget)
377 if h.parkView != nil {
378 c.ParkView = func(ctx context.Context, obs hostclaim.Observed) (park.View, error) { return h.parkView(ctx, obs) }
379 }
380 if inherited != nil && !h.ownAuthorize {
381 c.ParkAuthorize = inherited
382 }
383 if h.parkLeases != nil {
384 c.ParkLeases = h.parkLeases
385 }
386 if h.runHeld != nil {
387 c.ParkRunHeld = h.runHeld
388 }
389 c.OwnLease = h.leases.Acquire
390 if h.ownLease != nil {
391 c.OwnLease = h.ownLease
392 }
393 if h.store != nil {
394 c.Versioned = func(context.Context, string, string) (lease.VersionedStore, error) { return h.store, nil }
395 }
396 c.RenderHeld = func(_ context.Context, _ release.Store, _ registry.Route, _ string, pin *provision.MoveInputs, before func(string) error) (appconfig.HeldRender, error) {
397 h.renders++
398 if h.renderPin != nil {
399 h.renderPin(pin)
400 }
401 if h.renderHook != nil {
402 return h.renderHook(before)
403 }
404 hr, err := h.render()
405 if hr.SHA256 != "" {
406 if berr := before(hr.SHA256); berr != nil {
407 return appconfig.HeldRender{}, berr
408 }
409 }
410 return hr, err
411 }
412 c.ReleaseReady = func(context.Context, release.Store, provision.Record, string) moves.Check { return h.release }
413 c.ReleaseLive = h.releaseLive
414 c.ReplaceTimers = func(_ context.Context, host string, rows []registry.Timer) error {
415 h.replaced = append(h.replaced, append([]registry.Timer(nil), rows...))
416 return nil
417 }
418 c.SetReplication = func(host, class string) error { h.repl = append(h.repl, class); return nil }
419 c.SetNextDue = func(host string, due int64) error { h.nextDue = append(h.nextDue, due); return nil }
420 c.RemoveVerifyEntry = dbverify.RemoveHost
421 c.WriteGen = hostbox.WriteGenerationMarker
422 if h.probe != nil {
423 c.Probe = h.probe
424 }
425 if h.probeLive != nil {
426 c.ProbeLive = h.probeLive
427 }
428 c.ReleaseHeld = func(host, op string) (bool, error) {
429 h.openLog = append(h.openLog, "release:"+h.openState(t))
430 if h.releaseNoop {
431 return false, nil
432 }
433 if h.releaseErr != nil {
434 return false, h.releaseErr
435 }
436 if host != p1Host || !registry.IsHold(h.route.Status) || h.route.HoldOp != op {
437 return false, nil
438 }
439 h.route.Status, h.route.HoldOp = "ready", ""
440 return true, nil
441 }
442 if h.noRelease {
443 c.ReleaseHeld = nil
444 }
445 c.ConfirmProbeOpen = func(host, op string) bool {
446 h.openLog = append(h.openLog, "confirm:"+h.openState(t))
447 return host == p1Host && op == p1ID
448 }
449 c.MarkProbeOpen = func(host, op string) bool {
450 h.openLog = append(h.openLog, "mark:"+h.openState(t))
451 return host == p1Host && op == p1ID
452 }
453 c.landFault = func(point string) error {
454 h.landHits = append(h.landHits, point)
455 if h.holdProbed && point == "before-commit" {
456 return errHoldProbed
457 }
458 if h.landFault != nil {
459 if err := h.landFault(point); err != nil {
460 return err
461 }
462 }
463 if point == h.landAt && !h.landDone {
464 h.landDone = true
465 return errCrash
466 }
467 return nil
468 }
469 c.parkFault = func(step string) error {
470 h.parkHits = append(h.parkHits, step)
471 if step == h.parkAt && !h.parkDone {
472 h.parkDone = true
473 return errCrash
474 }
475 return nil
476 }
477 return c
478}
479
480// flip drives the cut to the flip: the record landing/checking in uk and
481// the claim landing/checking under uk-box.
482func (h *landingHarness) flip(t *testing.T) {
483 t.Helper()
484 h.ready(t)
485 h.cut(t)
486 h.pass(t)
487 if rec := h.read(t, p1Host); rec.Region != h.dst || rec.Move.State != provision.MoveLanding || rec.Move.LandingPhase != provision.LandChecking {
488 t.Fatalf("the fixture did not flip: %s %+v", rec.Region, rec.Move)
489 }
490}
491
492// land runs one pass as the target box, the worker kept across passes.
493func (h *landingHarness) land(t *testing.T) {
494 t.Helper()
495 if h.lw == nil {
496 h.lw = &movesWorker{mirrors: map[string]lease.MirrorStore{}}
497 }
498 h.lw.cfg = h.landingCfg(t)
499 if err := movesPass(context.Background(), h.s, h.lw); err != nil {
500 t.Fatal(err)
501 }
502}
503
504// crashLand runs one target pass that must reach the landing crash point.
505func (h *landingHarness) crashLand(t *testing.T) {
506 t.Helper()
507 h.land(t)
508 if !h.landDone {
509 t.Fatalf("the pass never reached %s (points reached: %v)", h.landAt, h.landHits)
510 }
511}
512
513// freshTarget drops the target worker, as a restarted agent would, and
514// with it every lease the dead process held: a real lease lapses at its
515// TTL once nobody renews it, and the restarted worker acquires afresh.
516func (h *landingHarness) freshTarget() {
517 h.lw = nil
518 h.leases.expire()
519}
520
521// expire lapses every held lease, as the TTL does for a dead holder, and
522// cancels every context derived from them.
523func (l *fakeParkLeases) expire() {
524 l.mu.Lock()
525 defer l.mu.Unlock()
526 l.held = map[string]bool{}
527 for _, h := range l.leases {
528 h.cancel()
529 }
530}
531
532func (h *landingHarness) landJournal(t *testing.T) *moves.Journal {
533 t.Helper()
534 j, err := moves.ReadJournal(h.tdir, p1Host, p1ID)
535 if err != nil {
536 t.Fatal(err)
537 }
538 return j
539}
540
541func (h *landingHarness) filesGen(t *testing.T) (filesGenMarker, bool) {
542 t.Helper()
543 body, err := os.ReadFile(filepath.Join(h.tdir, p1Host, archive.MarkerName))
544 if errors.Is(err, os.ErrNotExist) {
545 return filesGenMarker{}, false
546 }
547 if err != nil {
548 t.Fatal(err)
549 }
550 var m filesGenMarker
551 if err := json.Unmarshal(body, &m); err != nil {
552 t.Fatal(err)
553 }
554 return m, true
555}
556
557// installed asserts the end state of a landing through restored.
558func (h *landingHarness) installed(t *testing.T) moves.InstallManifest {
559 t.Helper()
560 rec := h.read(t, p1Host)
561 m := rec.Move
562 if m.State != provision.MoveLanding || m.LandingPhase != provision.LandRestored || m.Reason != "" {
563 t.Fatalf("record: %+v", m)
564 }
565 c := h.claim(t)
566 if c.Op.Kind != hostclaim.KindLanding || c.Op.Phase != provision.LandRestored || c.Op.Exec != h.dstBox || !containsString(c.Touched.Boxes, h.dstBox) {
567 t.Fatalf("claim: %+v touched %+v", c.Op, c.Touched)
568 }
569 j := h.landJournal(t)
570 var im moves.InstallManifest
571 if err := json.Unmarshal([]byte(j.Steps[landStepInstall]), &im); err != nil || im.Op != p1ID || im.N != h.n {
572 t.Fatalf("install manifest in the journal: %v %+v", err, im)
573 }
574 if m.Landing == nil || m.Landing.InstalledDBSHA256 != "sha256:"+im.DBSHA256 {
575 t.Fatalf("installed_db_sha256: %+v want %s", m.Landing, im.DBSHA256)
576 }
577 dbPath := provision.DefaultDBPath(h.tdir, p1Host)
578 if got, err := fileSHA256(dbPath); err != nil || got != "sha256:"+im.DBSHA256 {
579 t.Fatalf("the installed database: %v %s want %s", err, got, im.DBSHA256)
580 }
581 if got := countRowsForTest(t, dbPath); got != h.rows {
582 t.Fatalf("the installed database holds %d rows, the source wrote %d", got, h.rows)
583 }
584 if gen, err := hostbox.ReadGenerationMarker(dbPath + ".gen"); err != nil || gen != p1ID {
585 t.Fatalf("%s.gen: %q %v", dbPath, gen, err)
586 }
587 if fg, ok := h.filesGen(t); !ok || fg.Op != p1ID || fg.Incarnation != h.inc {
588 t.Fatalf(".files-gen: %+v present=%v", fg, ok)
589 }
590 if body, err := os.ReadFile(filepath.Join(h.tdir, p1Host, "upload.txt")); err != nil || string(body) != "hello" {
591 t.Fatalf("the extracted files: %q %v", body, err)
592 }
593 if body, err := os.ReadFile(filepath.Join(h.tdir, p1Host+".env")); err != nil || string(body) != h.srcEnvFile {
594 t.Fatalf("the extracted env: %q %v", body, err)
595 }
596 if held := h.leases.holding(); len(held) != 0 {
597 t.Fatalf("the landing still holds %v", held)
598 }
599 if _, err := os.Stat(filepath.Join(h.tdir, ".moves", p1Host, p1ID, "build", p1Host+".db")); !errors.Is(err, os.ErrNotExist) {
600 t.Fatalf("the build scratch was not renamed away: %v", err)
601 }
602 return im
603}
604
605// The end to end landing through restored: the generation built from the
606// seal, the archive extracted, timers replaced with their registrations,
607// the carried columns set, the verify entry gone, .files-gen and
608// <dbPath>.gen written, installed_db_sha256 recorded, the lease released.
609func TestLandingBuildsInstallsAndRestores(t *testing.T) {
610 t.Parallel()
611 ctx := context.Background()
612 h := newLandingHarness(t)
613 h.timers = []registry.Timer{{Host: p1Host, Name: "nightly", Kind: "cron", Spec: "0 3 * * *", NextAt: 1000, Attempts: 2, Registration: "reg-1"},
614 {Host: p1Host, Name: "once", Kind: "once", NextAt: 2000, DueAt: 2000, Registration: "reg-2"}}
615 h.flip(t)
616 h.route.Replication = ""
617 h.land(t)
618 im := h.installed(t)
619 // The generation holds exactly the build's four objects, at usable
620 // versions, in this box's own bucket.
621 prefix := moves.GenerationPrefix(cutRoot, p1ID)
622 var keys []string
623 for k := range h.f.buckets["data-uk"] {
624 if strings.HasPrefix(k, prefix) {
625 keys = append(keys, strings.TrimPrefix(k, prefix))
626 }
627 }
628 sort.Strings(keys)
629 want := []string{"0000/" + ltx.FormatFilename(ltx.TXID(h.n), ltx.TXID(h.n)), "0009/" + ltx.FormatFilename(1, ltx.TXID(h.n)), "build-intent.json", "install.json"}
630 if strings.Join(keys, " ") != strings.Join(want, " ") {
631 t.Fatalf("generation objects %v, want %v", keys, want)
632 }
633 if !lease.UsableVersionID(im.SnapshotVersion) || !lease.UsableVersionID(im.AnchorVersion) {
634 t.Fatalf("manifest versions %q %q", im.SnapshotVersion, im.AnchorVersion)
635 }
636 // Timers: the sealed snapshot's rows, registrations included, once.
637 if len(h.replaced) != 1 || len(h.replaced[0]) != 2 {
638 t.Fatalf("ReplaceTimers calls: %+v", h.replaced)
639 }
640 if moves.TimersDigest(h.replaced[0]) != h.read(t, p1Host).Move.Source.TimersSHA256 {
641 t.Fatalf("the replaced rows do not digest to the record's: %+v", h.replaced[0])
642 }
643 byName := map[string]registry.Timer{}
644 for _, r := range h.replaced[0] {
645 byName[r.Name] = r
646 }
647 if byName["nightly"].Registration != "reg-1" || byName["nightly"].Attempts != 2 || byName["once"].Registration != "reg-2" || byName["once"].DueAt != 2000 {
648 t.Fatalf("registrations and bookkeeping not preserved: %+v", h.replaced[0])
649 }
650 src := h.read(t, p1Host).Move.Source
651 if len(h.repl) != 1 || h.repl[0] != src.Replication || len(h.nextDue) != 1 || h.nextDue[0] != src.NextDue {
652 t.Fatalf("columns: replication %v next_due %v, source %+v", h.repl, h.nextDue, src)
653 }
654 dv, err := dbverify.Load(ctx, h.s)
655 if err != nil || dv == nil {
656 t.Fatal(err)
657 }
658 if _, ok := dv.Hosts[p1Host]; ok || len(dv.Hosts) != 1 {
659 t.Fatalf("the verify entry: %+v", dv.Hosts)
660 }
661 // The inventory names the four flat paths under F, the flip's stamp.
662 c := h.claim(t)
663 inv := c.Inventory["uk-box"]
664 if inv.Layout != "flat" || len(inv.Paths) != 4 {
665 t.Fatalf("inventory: %+v", inv)
666 }
667 for _, p := range []string{p1Host + ".db", p1Host + ".db.gen", p1Host, p1Host + ".env"} {
668 if inv.Paths[p] != c.Holder.Stamp {
669 t.Fatalf("inventory path %s at %d, want F %d", p, inv.Paths[p], c.Holder.Stamp)
670 }
671 }
672 captured, err := hostguard.Capture(h.tdir, p1Host)
673 if err != nil || captured.Stamp != c.Holder.Stamp || captured.Incarnation != h.inc {
674 t.Fatalf("the installation record: %+v %v, want stamp %d", captured, err, c.Holder.Stamp)
675 }
676 // No residue, so no park chores; and no park-local (no earlier stay).
677 if len(c.Obligations) != 0 {
678 t.Fatalf("obligations: %+v", c.Obligations)
679 }
680 // The journal records every sub-step, the prepared manifest as skipped.
681 j := h.landJournal(t)
682 for _, step := range []string{landStepResidue, landStepInstall, landStepPublished, landStepRetired, landStepExtracted, landStepInventory, landStepTimers, landStepColumns, landStepVerify, landStepFilesManifest, landStepFilesGen} {
683 if !j.Done(step) {
684 t.Fatalf("journal lacks %s: %v", step, j.Steps)
685 }
686 }
687 if h.renders != 1 {
688 t.Fatalf("renders %d", h.renders)
689 }
690 // Restored is Plan 3's: another pass changes nothing.
691 before := h.read(t, p1Host).Move.UpdatedAt
692 h.land(t)
693 if after := h.read(t, p1Host).Move; after.LandingPhase != provision.LandRestored || !after.UpdatedAt.Equal(before) {
694 t.Fatalf("a pass at restored wrote the record: %+v", after)
695 }
696}
697
698// checking (§7.6): the claim must be this move's landing op with this box
699// executing, citing complete source evidence, and this box is added to
700// touched.boxes before any local write; the installation record is
701// activated at F. A claim the source left at moving/sealed (a crash after
702// the record's flip) is flipped by the target itself (ruling G12).
703func TestLandingCheckingRequiresClaimEvidenceAndTouch(t *testing.T) {
704 ctx := context.Background()
705 t.Run("the target writes the flip's claim half", func(t *testing.T) {
706 h := newLandingHarness(t)
707 h.ready(t)
708 h.crashAt = "after-record-flip"
709 h.cut(t)
710 h.crashPass(t)
711 if c := h.claim(t); c.Op.Kind != hostclaim.KindMoving || c.Op.Phase != hostclaim.PhaseSealed {
712 t.Fatalf("fixture: %+v", c.Op)
713 }
714 h.landAt = "after-target-flip"
715 h.crashLand(t)
716 c := h.claim(t)
717 if c.Op.Kind != hostclaim.KindLanding || c.Op.Phase != provision.LandChecking || c.Op.Exec != "uk-box" {
718 t.Fatalf("after the target's flip: %+v", c.Op)
719 }
720 if c.Op.Journal[hostclaim.JournalSourceStamp] != "1" || c.Op.Journal[hostclaim.JournalSrcGen] != "cur" {
721 t.Fatalf("the target's flip did not journal the source's stamp and generation: %+v", c.Op.Journal)
722 }
723 h.land(t)
724 h.installed(t)
725 })
726 t.Run("another executor is refused", func(t *testing.T) {
727 h := newLandingHarness(t)
728 h.flip(t)
729 // Refused before any claim write: with uk-box taken out of touched,
730 // the K12 Touch that follows the executor check must not run (the
731 // guard would refuse the activation too, but later and locally).
732 rewriteP1Claim(t, h.s, func(c *hostclaim.Claim) { c.Op.Exec = "other-box"; c.Touched.Boxes = []string{"ie-box"} })
733 h.land(t)
734 if m := h.read(t, p1Host).Move; m.LandingPhase != provision.LandChecking {
735 t.Fatalf("advanced under another executor: %+v", m)
736 }
737 if _, err := hostguard.Capture(h.tdir, p1Host); !errors.Is(err, hostguard.ErrNoRecord) {
738 t.Fatalf("the installation record was written: %v", err)
739 }
740 if c := h.claim(t); containsString(c.Touched.Boxes, "uk-box") {
741 t.Fatal("a claim write under another executor")
742 }
743 })
744 t.Run("missing evidence is refused", func(t *testing.T) {
745 h := newLandingHarness(t)
746 h.flip(t)
747 if err := h.s.Delete(ctx, h.stoppedKey()); err != nil {
748 t.Fatal(err)
749 }
750 h.land(t)
751 if m := h.read(t, p1Host).Move; m.LandingPhase != provision.LandChecking {
752 t.Fatalf("advanced without the source's evidence: %+v", m)
753 }
754 })
755 t.Run("touch and the installation record", func(t *testing.T) {
756 h := newLandingHarness(t)
757 h.flip(t)
758 // Plan 1's target report touched uk-box at its first staging write;
759 // checking's Touch is the idempotent K12 re-add, so take it away
760 // first to see it written.
761 rewriteP1Claim(t, h.s, func(c *hostclaim.Claim) { c.Touched.Boxes = []string{"ie-box"} })
762 h.landAt = "after-record-parking"
763 h.crashLand(t)
764 c := h.claim(t)
765 if !containsString(c.Touched.Boxes, "uk-box") || !containsString(c.Touched.Regions, "ie") {
766 t.Fatalf("touched: %+v", c.Touched)
767 }
768 captured, err := hostguard.Capture(h.tdir, p1Host)
769 if err != nil || captured.Stamp != c.Holder.Stamp || captured.Incarnation != h.inc {
770 t.Fatalf("active_stamp %+v %v, want F %d", captured, err, c.Holder.Stamp)
771 }
772 // Spec §7.6 checking (Plan 5 Task 9a, M5): the location's lease in
773 // the target's own bucket, taken once and held into parking.
774 want := "data-uk/" + strings.TrimSuffix(c.Holder.Location, "/")
775 if len(h.leases.acquired) != 1 || h.leases.acquired[0] != want {
776 t.Fatalf("checking took %v, want the location's lease %s", h.leases.acquired, want)
777 }
778 if held := h.leases.holding(); len(held) != 1 || held[0] != want {
779 t.Fatalf("after checking the landing holds %v", held)
780 }
781 })
782 t.Run("a nested row is refused for a rollback", func(t *testing.T) {
783 h := newLandingHarness(t)
784 h.flip(t)
785 h.route.DBPath = filepath.Join(h.tdir, "instances", p1Host, "db", "instance.db")
786 h.land(t)
787 if m := h.read(t, p1Host).Move; m.Reason != provision.RollbackLandingLayout || m.LandingPhase != provision.LandChecking || m.State != provision.MoveLanding {
788 t.Fatalf("a nested row: %+v", m)
789 }
790 })
791 t.Run("host claims on refuse at checking", func(t *testing.T) {
792 h := newLandingHarness(t)
793 h.flip(t)
794 t.Setenv("CARLOS_HOST_CLAIM", "1")
795 h.land(t)
796 if m := h.read(t, p1Host).Move; m.Reason != provision.RollbackClaimWired || m.LandingPhase != provision.LandChecking {
797 t.Fatalf("under CARLOS_HOST_CLAIM: %+v", m)
798 }
799 })
800}
801
802// residue plants an earlier stay of this incarnation at the holder's
803// location in the target's bucket: two generations (old and the origin)
804// and a generation.json, each at a usable version.
805func (h *landingHarness) residue(t *testing.T) []string {
806 t.Helper()
807 ctx := context.Background()
808 keys := []string{
809 cutRoot + "/g-old/0000/0000000000000001-0000000000000001.ltx",
810 cutRoot + "/g-old/0009/0000000000000001-0000000000000001.ltx",
811 cutRoot + "/0000/0000000000000001-0000000000000001.ltx",
812 cutRoot + "/generation.json",
813 }
814 for _, k := range keys {
815 if _, err := h.vs.PutIfAbsent(ctx, h.dstBucket, k, []byte("residue "+k)); err != nil {
816 t.Fatal(err)
817 }
818 }
819 return keys
820}
821
822// parking (§7.6, D6): an earlier stay's objects under holder.location are
823// archived under parked/<inc>/<host>/<ret>/instances/... by one
824// park-generation chore per generation before building; a crash inside the
825// engine leaves the phase at parking and the replay finishes; the chores
826// stay pending until the horizon; after the probe makes the new generation
827// live here, Engine.Run on them acknowledges rather than refusing.
828func TestLandingParkingWaitsForTheManifest(t *testing.T) {
829 t.Parallel()
830 ctx := context.Background()
831 h := newLandingHarness(t)
832 h.flip(t)
833 keys := h.residue(t)
834 h.parkAt = "copy"
835 h.land(t)
836 if !h.parkDone {
837 t.Fatalf("the engine never copied (%v)", h.parkHits)
838 }
839 if m := h.read(t, p1Host).Move; m.LandingPhase != provision.LandParking {
840 t.Fatalf("after the crash: %+v", m)
841 }
842 c := h.claim(t)
843 var gens []string
844 for _, o := range c.Obligations {
845 if o.Op == p1ID && o.Kind == "park-generation" && o.Region == "uk" && o.Park != nil && o.Park.Location == cutRoot && o.Park.EvidenceAt.Equal(h.read(t, p1Host).Move.FlippedAt.UTC()) {
846 gens = append(gens, o.Generation)
847 }
848 }
849 sort.Strings(gens)
850 if strings.Join(gens, " ") != hostclaim.RootGeneration+" old" {
851 t.Fatalf("residue chores: %v (%+v)", gens, c.Obligations)
852 }
853 if len(h.leases.acquired) == 0 {
854 t.Fatal("the engine took no lease")
855 }
856 // The replay archives and writes the manifest, but one delete the store
857 // accepts does not land (its listing still shows the version): the
858 // manifest exists and its deletion set is not gone, so Parked is false
859 // and the phase waits, chores queued, nothing built.
860 stuck := keys[0]
861 h.vs.skipDelete = func(key string) bool { return key == stuck }
862 h.land(t)
863 if m := h.read(t, p1Host).Move; m.LandingPhase != provision.LandParking {
864 t.Fatalf("advanced with a deletion outstanding: %+v", m)
865 }
866 if _, ok := h.f.buckets["data-uk"][stuck]; !ok {
867 t.Fatalf("fixture: %s was deleted", stuck)
868 }
869 manifests := 0
870 for k := range h.f.buckets["data-uk"] {
871 if strings.HasPrefix(k, "parked/"+h.inc+"/"+p1Host+"/") && strings.HasSuffix(k, "/manifest.json") {
872 manifests++
873 }
874 }
875 if manifests != 2 {
876 t.Fatalf("%d park manifests after the second pass", manifests)
877 }
878 if n := len(h.vs.callsOn(moves.GenerationPrefix(cutRoot, p1ID))); n != 0 {
879 t.Fatalf("the build ran while parking waited: %d calls", n)
880 }
881 // The deletes land: the deletion set is gone, the landing goes on to
882 // restored. The chores are still owed.
883 h.vs.skipDelete = nil
884 h.land(t)
885 h.installed(t)
886 c = h.claim(t)
887 var retirements []string
888 for _, o := range c.Obligations {
889 if o.Kind == "park-generation" {
890 retirements = append(retirements, o.Retirement)
891 }
892 }
893 if len(retirements) != 2 {
894 t.Fatalf("chores after the landing: %+v", c.Obligations)
895 }
896 // The marker is the location's one reused key: it is not parked (the
897 // probe supersedes it), every other object is.
898 marker := cutRoot + "/generation.json"
899 if _, ok := h.f.buckets["data-uk"][marker]; !ok {
900 t.Fatal("the earlier stay's generation.json was parked")
901 }
902 for _, k := range keys {
903 if _, ok := h.f.buckets["data-uk"][k]; ok && k != marker {
904 t.Fatalf("%s is still at the location", k)
905 }
906 }
907 var archived []string
908 for k := range h.f.buckets["data-uk"] {
909 if strings.HasPrefix(k, "parked/"+h.inc+"/"+p1Host+"/") {
910 archived = append(archived, k)
911 }
912 }
913 sort.Strings(archived)
914 if len(archived) != len(keys)-1+2 { // every object but the marker, plus two manifests
915 t.Fatalf("archived: %v", archived)
916 }
917 for _, k := range keys {
918 if k == marker {
919 continue
920 }
921 rel := strings.TrimPrefix(k, "instances/")
922 found := false
923 for _, a := range archived {
924 if strings.Contains(a, "/instances/"+strings.TrimPrefix(rel, p1Host+"/")+"@") {
925 found = true
926 }
927 }
928 if !found {
929 t.Fatalf("%s was not archived under parked/<inc>/<host>/<ret>/instances/ (%v)", k, archived)
930 }
931 }
932 // The probe's admission (Plan 3) makes g-<move id> live here: the
933 // holder's generation is the move id, the marker names it and pins the
934 // snapshot, and uk is authoritative. Past the horizon the chores
935 // acknowledge: their generations are not the live one.
936 j := h.landJournal(t)
937 var im moves.InstallManifest
938 if err := json.Unmarshal([]byte(j.Steps[landStepInstall]), &im); err != nil {
939 t.Fatal(err)
940 }
941 // The probe writes the marker If-Match over the earlier stay's: a new
942 // current version at the same key.
943 markerVer := h.f.nextVersion()
944 h.f.store("data-uk", marker, fakeMoveObject{lease.MirrorObject{ETag: "etag-" + markerVer, Size: 20}, `{"op":"` + p1ID + `"}`})
945 h.f.setVersion("data-uk", marker, markerVer)
946 rewriteP1Claim(t, h.s, func(c *hostclaim.Claim) { c.Holder.Generation = p1ID })
947 h.clock.Advance(park.SignatureAge + landingRequestLimit + time.Minute)
948 view := func(_ context.Context, obs hostclaim.Observed) (park.View, error) {
949 return park.View{ClaimETag: obs.ETag, Region: "uk", AuthoritativeRegion: "uk", MarkerKnown: true, MarkerGeneration: p1ID,
950 Marker: park.Ref{Key: cutRoot + "/generation.json", VersionID: markerVer},
951 Pinned: map[park.Ref]bool{{Key: im.SnapshotKey, VersionID: im.SnapshotVersion}: true},
952 GenerationOf: func(r park.Ref) (string, bool) { return landingGenerationOf(cutRoot, r.Key) }}, nil
953 }
954 l := &landingRun{w: h.lw, s: h.s, rec: h.read(t, p1Host), g: provision.MoveGuard{ID: p1ID, Incarnation: h.inc}}
955 eng := park.Engine{Store: h.vs, Claims: h.s, Bucket: "data-uk", Region: "uk", Leases: h.leases, Now: h.clock.Now, NewID: obligationID, View: view, Authorize: l.authorizeObligation}
956 for _, o := range c.Obligations {
957 if o.Kind != "park-generation" {
958 continue
959 }
960 if err := eng.Run(ctx, park.Work{Host: p1Host, ID: o.ID, Op: p1ID}); err != nil {
961 t.Fatalf("acknowledge generation %s: %v", o.Generation, err)
962 }
963 }
964 if c := h.claim(t); len(c.Obligations) != 0 {
965 t.Fatalf("chores after the acknowledgement: %+v", c.Obligations)
966 }
967 prefix := moves.GenerationPrefix(cutRoot, p1ID)
968 n := 0
969 for k := range h.f.buckets["data-uk"] {
970 if strings.HasPrefix(k, prefix) {
971 n++
972 }
973 }
974 if n != 4 {
975 t.Fatalf("the acknowledgement touched the live generation: %d objects left", n)
976 }
977 if held := h.leases.holding(); len(held) != 0 {
978 t.Fatalf("leases held after the acknowledgement: %v", held)
979 }
980}
981
982// Phase catch-up (§7.5, round-2 finding 11): a crash after the record's CAS
983// and before the claim's leaves the claim one step behind; the next pass
984// walks the claim forward and continues. A claim several steps behind is
985// walked step by step, with each transition written separately.
986func TestLandingPhaseCatchUp(t *testing.T) {
987 t.Parallel()
988 ctx := context.Background()
989 h := newLandingHarness(t)
990 h.flip(t)
991 h.landAt = "after-record-parking"
992 h.crashLand(t)
993 if m := h.read(t, p1Host).Move; m.LandingPhase != provision.LandParking {
994 t.Fatalf("record: %+v", m)
995 }
996 if c := h.claim(t); c.Op.Phase != provision.LandChecking {
997 t.Fatalf("the claim moved with the record: %+v", c.Op)
998 }
999 h.freshTarget()
1000 h.landAt = "after-residue-queued" // never reached: no residue
1001 h.landDone = false
1002 h.land(t)
1003 h.installed(t)
1004
1005 // A claim left at checking with the record at restored.
1006 rewriteClaimOp(t, h.s, func(op *hostclaim.Op) { op.Phase = provision.LandChecking })
1007 l := &landingRun{w: h.lw, s: h.s, rec: h.read(t, p1Host), g: provision.MoveGuard{ID: p1ID, Incarnation: h.inc}}
1008 obs, err := l.syncClaimPhase(ctx)
1009 if err != nil {
1010 t.Fatal(err)
1011 }
1012 if obs.Claim.Op.Phase != provision.LandRestored || h.claim(t).Op.Phase != provision.LandRestored {
1013 t.Fatalf("after the catch-up: %+v", obs.Claim.Op)
1014 }
1015 // A claim ahead of the record is left alone.
1016 rewriteClaimOp(t, h.s, func(op *hostclaim.Op) { op.Phase = provision.LandProbing })
1017 if obs, err := l.syncClaimPhase(ctx); err != nil || obs.Claim.Op.Phase != provision.LandProbing {
1018 t.Fatalf("a claim ahead: %v %+v", err, obs.Claim.Op)
1019 }
1020 // A claim at another kind is an error.
1021 rewriteClaimOp(t, h.s, func(op *hostclaim.Op) { op.Kind = hostclaim.KindRestoring; op.Phase = "released" })
1022 if _, err := l.syncClaimPhase(ctx); err == nil {
1023 t.Fatal("a restoring claim was accepted")
1024 }
1025}
1026
1027// earlierStay plants an earlier stay of this incarnation on the target box:
1028// the installation record at stamp 1, the inventory listing the four flat
1029// paths at that stamp, and the files at the live paths.
1030func (h *landingHarness) earlierStay(t *testing.T) {
1031 t.Helper()
1032 rec := fmt.Sprintf(`{"v":1,"host":%q,"incarnation":%q,"active_stamp":1,"retired_stamps":[]}`, p1Host, h.inc)
1033 if err := os.WriteFile(filepath.Join(h.tdir, "hostguard", p1Host+".json"), []byte(rec), 0o660); err != nil {
1034 t.Fatal(err)
1035 }
1036 rewriteP1Claim(t, h.s, func(c *hostclaim.Claim) {
1037 c.Inventory = map[string]hostclaim.InventoryEntry{h.dstBox: {Layout: "flat", Paths: map[string]uint64{p1Host + ".db": 1, p1Host + ".db.gen": 1, p1Host: 1, p1Host + ".env": 1}}}
1038 })
1039 if err := os.MkdirAll(filepath.Join(h.tdir, p1Host), 0o755); err != nil {
1040 t.Fatal(err)
1041 }
1042 for name, body := range map[string]string{p1Host + ".db": "old database", p1Host + ".db.gen": "old\n", p1Host + ".env": "OLD=1\n", filepath.Join(p1Host, "old.txt"): "old files"} {
1043 if err := os.WriteFile(filepath.Join(h.tdir, name), []byte(body), 0o644); err != nil {
1044 t.Fatal(err)
1045 }
1046 }
1047}
1048
1049// local's order (round-2 finding 8, rulings G5 and G15): an earlier stay's
1050// artifacts tagged S' are parked into .parked/ and gone from the live paths
1051// before the new installation is registered under F; registering first is
1052// what the real RecordInventory refuses. A park-local is queued at checking
1053// only because the inventory has paths at S'.
1054func TestLandingInventoryOrder(t *testing.T) {
1055 t.Parallel()
1056 ctx := context.Background()
1057 h := newLandingHarness(t)
1058 h.flip(t)
1059 h.earlierStay(t)
1060 // Registering first is refused: with the record activated at F and the
1061 // old paths still tagged 1, RecordInventory says ErrLocalInventory.
1062 h.landAt = "after-activate"
1063 h.crashLand(t)
1064 c := h.claim(t)
1065 if !hostclaim.InventoryHasStamp(c, "uk-box", 1) {
1066 t.Fatal("fixture: no S' paths")
1067 }
1068 if j := h.landJournal(t); j.Steps[landStepPrevious] != "1" {
1069 t.Fatalf("previous: %v", j.Steps)
1070 }
1071 captured, err := hostguard.Capture(h.tdir, p1Host)
1072 if err != nil || captured.Stamp != c.Holder.Stamp {
1073 t.Fatalf("captured %+v %v", captured, err)
1074 }
1075 local := park.Local{Guard: hostguard.Guard{DataDir: h.tdir, LockDir: t.TempDir(), Claims: hostguard.StoreReader{Store: h.s}}, Claims: h.s, DataDir: h.tdir, Box: "uk-box"}
1076 check := func(c *hostclaim.Claim) error { return nil }
1077 if err := local.RecordInventory(ctx, p1Host, captured, "flat", []string{p1Host + ".db"}, check); !errors.Is(err, park.ErrLocalInventory) {
1078 t.Fatalf("registering over S' paths: %v", err)
1079 }
1080 // The landing: the park-local chore is queued, local retires then registers.
1081 h.land(t)
1082 h.installed(t)
1083 c = h.claim(t)
1084 if len(c.Obligations) != 0 {
1085 t.Fatalf("the park-local was not completed: %+v", c.Obligations)
1086 }
1087 inv := c.Inventory["uk-box"]
1088 for p, s := range inv.Paths {
1089 if s != c.Holder.Stamp {
1090 t.Fatalf("inventory %s at %d, want F %d", p, s, c.Holder.Stamp)
1091 }
1092 }
1093 // The old artifacts are under .parked/<inc>/<host>/<ret>/artifacts/, and
1094 // the live paths hold the new installation.
1095 var parked []string
1096 filepath.WalkDir(filepath.Join(h.tdir, ".parked", h.inc, p1Host), func(p string, d os.DirEntry, err error) error {
1097 if err == nil && !d.IsDir() && !strings.HasSuffix(p, ".journal.json") {
1098 rel, _ := filepath.Rel(filepath.Join(h.tdir, ".parked", h.inc, p1Host), p)
1099 parked = append(parked, strings.Join(strings.Split(rel, string(filepath.Separator))[1:], "/"))
1100 }
1101 return nil
1102 })
1103 sort.Strings(parked)
1104 wantParked := []string{"artifacts/" + p1Host + ".db", "artifacts/" + p1Host + ".db.gen", "artifacts/" + p1Host + ".env", "artifacts/" + p1Host + "/old.txt"}
1105 if strings.Join(parked, " ") != strings.Join(wantParked, " ") {
1106 t.Fatalf("parked: %v, want %v", parked, wantParked)
1107 }
1108 if _, err := os.Stat(filepath.Join(h.tdir, p1Host, "old.txt")); !errors.Is(err, os.ErrNotExist) {
1109 t.Fatalf("the old files are still live: %v", err)
1110 }
1111 if body, err := os.ReadFile(filepath.Join(h.tdir, p1Host+".env")); err != nil || string(body) != "APP_MODE=prod\n" {
1112 t.Fatalf("env: %q %v", body, err)
1113 }
1114}
1115
1116// No earlier stay queues no park-local and runs no retirement (ruling G5),
1117// and the first landing on a box completes end to end with the real
1118// park.Local and hostguard.Guard.
1119func TestLandingFirstStayQueuesNoLocalPark(t *testing.T) {
1120 t.Parallel()
1121 h := newLandingHarness(t)
1122 h.flip(t)
1123 h.landAt = "after-record-parking"
1124 h.crashLand(t)
1125 if c := h.claim(t); len(c.Obligations) != 0 {
1126 t.Fatalf("a first landing queued %+v", c.Obligations)
1127 }
1128 if j := h.landJournal(t); j.Done(landStepPrevious) {
1129 t.Fatalf("a first landing noted a previous stay: %v", j.Steps)
1130 }
1131 h.land(t)
1132 h.installed(t)
1133}
1134
1135// The markers are written last (witness wMarkersLast, codex R2-6): a crash
1136// before local's final sub-step leaves no .files-gen with everything else
1137// durable; the replay finishes local and the landing.
1138func TestLandingMarkersAreWrittenLast(t *testing.T) {
1139 t.Parallel()
1140 h := newLandingHarness(t)
1141 h.flip(t)
1142 h.landAt = "before-files-gen"
1143 h.crashLand(t)
1144 if _, ok := h.filesGen(t); ok {
1145 t.Fatal(".files-gen exists before the last sub-step")
1146 }
1147 if m := h.read(t, p1Host).Move; m.LandingPhase != provision.LandLocal {
1148 t.Fatalf("record: %+v", m)
1149 }
1150 if body, err := os.ReadFile(filepath.Join(h.tdir, p1Host, "upload.txt")); err != nil || string(body) != "hello" {
1151 t.Fatalf("the files were not extracted before the marker: %q %v", body, err)
1152 }
1153 if _, err := os.Stat(provision.DefaultDBPath(h.tdir, p1Host) + ".gen"); !errors.Is(err, os.ErrNotExist) {
1154 t.Fatalf("<dbPath>.gen before restored: %v", err)
1155 }
1156 h.freshTarget()
1157 h.land(t)
1158 h.installed(t)
1159 if len(h.replaced) != 1 {
1160 t.Fatalf("the replay replaced the timers again: %d", len(h.replaced))
1161 }
1162}
1163
1164// configured (§7.6, Review Focus 4): the held render's bundle and overlay
1165// etags must be the pinned inputs; a difference marks the rollback with
1166// "configuration changed during the move" and the landing stops there. The
1167// release gate waits.
1168func TestLandingConfiguredPinsInputs(t *testing.T) {
1169 t.Parallel()
1170 h := newLandingHarness(t)
1171 h.flip(t)
1172 h.render = func() (appconfig.HeldRender, error) {
1173 return appconfig.HeldRender{BundleETags: map[string]string{"default": "e2", "environment": ""}}, nil
1174 }
1175 h.land(t)
1176 m := h.read(t, p1Host).Move
1177 if m.Reason != provision.RollbackConfigChanged || m.LandingPhase != provision.LandConfigured || m.State != provision.MoveLanding {
1178 t.Fatalf("after the render: %+v", m)
1179 }
1180 if _, err := os.Stat(provision.DefaultDBPath(h.tdir, p1Host)); !errors.Is(err, os.ErrNotExist) {
1181 t.Fatalf("the database was installed under a mismatch: %v", err)
1182 }
1183 // The next pass rolls the marked landing back (Plan 4's decision, J10),
1184 // installing nothing.
1185 h.land(t)
1186 rolledBack(t, h, provision.RollbackConfigChanged, provision.LandConfigured)
1187 if _, err := os.Stat(provision.DefaultDBPath(h.tdir, p1Host)); !errors.Is(err, os.ErrNotExist) {
1188 t.Fatalf("the rollback installed the database: %v", err)
1189 }
1190
1191 h2 := newLandingHarness(t)
1192 h2.flip(t)
1193 h2.release = moves.Check{Reason: t7Waiting}
1194 h2.land(t)
1195 if m := h2.read(t, p1Host).Move; m.LandingPhase != provision.LandConfigured || m.Reason != "" {
1196 t.Fatalf("waiting on the release: %+v", m)
1197 }
1198 h2.release = moves.Check{OK: true}
1199 h2.land(t)
1200 h2.installed(t)
1201}
1202
1203// landing_deadline passed rolls the landing back (Plan 4's decision, J11),
1204// at whatever phase the landing is, restored included.
1205func TestLandingDeadlineRollsBack(t *testing.T) {
1206 t.Parallel()
1207 h := newLandingHarness(t)
1208 h.flip(t)
1209 h.clock.Advance(cutLandingWindow + time.Minute)
1210 h.land(t)
1211 rolledBack(t, h, provision.RollbackLandingDeadline, provision.LandChecking)
1212
1213 h2 := newLandingHarness(t)
1214 h2.flip(t)
1215 h2.release = moves.Check{Reason: t7NotProven}
1216 h2.land(t)
1217 if m := h2.read(t, p1Host).Move; m.LandingPhase != provision.LandConfigured || m.Reason != "" {
1218 t.Fatalf("waiting on the proof: %+v", m)
1219 }
1220 h2.clock.Advance(cutLandingWindow + time.Minute)
1221 h2.land(t)
1222 rolledBack(t, h2, provision.RollbackLandingDeadline, provision.LandConfigured)
1223
1224 h3 := newLandingHarness(t)
1225 h3.flip(t)
1226 h3.land(t)
1227 h3.installed(t)
1228 h3.clock.Advance(cutLandingWindow + time.Minute)
1229 h3.land(t)
1230 rolledBack(t, h3, provision.RollbackLandingDeadline, provision.LandRestored)
1231}
1232
1233// Every phase is replay-safe: a crash after each durable side effect, then
1234// a fresh worker to restored, yields one object per generation key, one
1235// timers replacement, one install manifest, and the same end state.
1236func TestLandingReplaysArePhaseIdempotent(t *testing.T) {
1237 slots := make(chan struct{}, 4)
1238 points := []string{"after-previous", "after-activate", "after-record-parking", "after-residue-queued", "after-record-building", "after-build",
1239 "after-record-published", "after-record-local", "after-retired", "after-staged", "after-files-rename", "after-files_placed", "after-extracted", "after-inventory", "after-timers",
1240 "after-columns", "after-verify", "after-files_manifest", "before-files-gen", "after-files_gen", "after-record-configured", "after-release-ready", "after-db-rename"}
1241 for _, point := range points {
1242 t.Run(point, func(t *testing.T) {
1243 t.Parallel()
1244 slots <- struct{}{}
1245 defer func() { <-slots }()
1246 h := newLandingHarness(t)
1247 h.flip(t)
1248 h.earlierStay(t)
1249 h.residue(t)
1250 h.landAt = point
1251 h.crashLand(t)
1252 h.freshTarget()
1253 h.land(t)
1254 h.installed(t)
1255 if len(h.replaced) != 1 {
1256 t.Fatalf("timers replaced %d times", len(h.replaced))
1257 }
1258 prefix := moves.GenerationPrefix(cutRoot, p1ID)
1259 n := 0
1260 for k := range h.f.buckets["data-uk"] {
1261 if strings.HasPrefix(k, prefix) {
1262 n++
1263 }
1264 }
1265 if n != 4 {
1266 t.Fatalf("%d generation objects", n)
1267 }
1268 c := h.claim(t)
1269 gens := 0
1270 for _, o := range c.Obligations {
1271 switch o.Kind {
1272 case "park-generation":
1273 gens++
1274 default:
1275 t.Fatalf("obligation left: %+v", o)
1276 }
1277 }
1278 if gens != 2 {
1279 t.Fatalf("%d residue chores", gens)
1280 }
1281 for p, s := range c.Inventory["uk-box"].Paths {
1282 if s != c.Holder.Stamp {
1283 t.Fatalf("inventory %s at %d", p, s)
1284 }
1285 }
1286 })
1287 }
1288}
1289
1290// landedPreparedManifest reads the prepared manifest the landing's local
1291// phase journalled (Task 6, C8): the binding from the op journal, the
1292// manifest at its key in the target's bucket, decoded.
1293func landedPreparedManifest(t *testing.T, h *landingHarness) (preparedManifest, hostfiles.Manifest, string) {
1294 t.Helper()
1295 raw, ok := h.landJournal(t).Steps[landStepFilesManifest]
1296 if !ok || raw == landingFilesManifestSkipped {
1297 t.Fatalf("files_manifest: %q", raw)
1298 }
1299 var pm preparedManifest
1300 if err := json.Unmarshal([]byte(raw), &pm); err != nil {
1301 t.Fatalf("files_manifest %q: %v", raw, err)
1302 }
1303 obj, ok := h.f.buckets[h.dstBucket][pm.Key]
1304 if !ok {
1305 t.Fatalf("no manifest at %s in the target's bucket", pm.Key)
1306 }
1307 if got := sha([]byte(obj.body)); got != pm.SHA256 {
1308 t.Fatalf("the manifest at %s is not the journalled bytes: %s", pm.Key, got)
1309 }
1310 var m hostfiles.Manifest
1311 if err := json.Unmarshal([]byte(obj.body), &m); err != nil {
1312 t.Fatal(err)
1313 }
1314 return pm, m, obj.body
1315}
1316
1317// The prepared files manifest (spec rev 7.22 §7.6, R19-1; C8): local
1318// writes it under files/g-<op>/ in the location, bound to {location,
1319// g-<op>, op, stamp} in the op journal, at sequence 0, while
1320// holder.generation is still the source's. It carries the placed files,
1321// <host>.env and the wake state the landing installed.
1322func TestLocalBindsThePreparedManifestToTheNewGeneration(t *testing.T) {
1323 t.Parallel()
1324 h := newLandingHarness(t)
1325 h.flip(t)
1326 h.land(t)
1327 h.installed(t)
1328 c := h.claim(t)
1329 if c.Holder.Generation != "cur" {
1330 t.Fatalf("holder.generation moved before the probe: %q", c.Holder.Generation)
1331 }
1332 pm, m, _ := landedPreparedManifest(t, h)
1333 prefix := movesFilesPrefix(c.Holder.Location, p1ID)
1334 if pm.Key != hostfiles.ManifestKey(prefix) || pm.Op != p1ID || pm.Stamp != c.Holder.Stamp {
1335 t.Fatalf("binding: %+v, want key %s op %s stamp %d", pm, hostfiles.ManifestKey(prefix), p1ID, c.Holder.Stamp)
1336 }
1337 vs, err := h.vs.ListVersions(context.Background(), "data-uk", pm.Key)
1338 if err != nil {
1339 t.Fatal(err)
1340 }
1341 exact := ""
1342 for _, v := range vs {
1343 if v.Key == pm.Key && v.IsLatest {
1344 exact = v.VersionID
1345 }
1346 }
1347 if pm.Version == "" || pm.Version != exact {
1348 t.Fatalf("the binding's version %q is not the manifest's exact version %q", pm.Version, exact)
1349 }
1350 if m.Seq != 0 {
1351 t.Fatalf("prepared manifest at seq %d", m.Seq)
1352 }
1353 if err := hostfiles.ValidateManifest(m, []string{prefix}); err != nil {
1354 t.Fatal(err)
1355 }
1356 dir := filepath.Join(h.tdir, p1Host)
1357 placed := 0
1358 if err := filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error {
1359 if err != nil || d.IsDir() {
1360 return err
1361 }
1362 rel, _ := filepath.Rel(dir, path)
1363 if rel == archive.MarkerName {
1364 return nil // .files-gen is written after the manifest, so it is not in it
1365 }
1366 body, err := os.ReadFile(path)
1367 if err != nil {
1368 return err
1369 }
1370 placed++
1371 if e := m.Entries[filepath.ToSlash(rel)]; e.SHA256 != sha(body) || e.Seq != 0 {
1372 t.Errorf("%s: %+v", rel, e)
1373 }
1374 return nil
1375 }); err != nil {
1376 t.Fatal(err)
1377 }
1378 if placed == 0 || len(m.Entries) != placed {
1379 t.Fatalf("%d entries for %d placed files: %+v", len(m.Entries), placed, m.Entries)
1380 }
1381 env, err := os.ReadFile(filepath.Join(h.tdir, p1Host+".env"))
1382 if err != nil {
1383 t.Fatal(err)
1384 }
1385 if e := m.Platform[hostfiles.EnvItem]; e.SHA256 != sha(env) {
1386 t.Fatalf("env item %+v is not <host>.env", e)
1387 }
1388 wobj, ok := h.f.buckets["data-uk"][m.Platform[hostfiles.WakeItem].Object]
1389 if !ok {
1390 t.Fatal("no wake state object")
1391 }
1392 var ws hostfiles.WakeState
1393 if err := json.Unmarshal([]byte(wobj.body), &ws); err != nil {
1394 t.Fatal(err)
1395 }
1396 if ws.V != 1 || ws.Host != p1Host || len(h.replaced) != 1 || len(ws.Timers) != len(h.replaced[0]) || ws.NextDue != h.nextDue[0] || ws.Replication != h.repl[0] {
1397 t.Fatalf("wake state %+v, timers replaced %v, next_due %v, replication %v", ws, h.replaced, h.nextDue, h.repl)
1398 }
1399}
1400
1401// A journal that records the step as skipped (written before C8 existed)
1402// counts as not done (ruling Q3 item 2): a replay builds the manifest and
1403// the landing goes on with the binding in its journal.
1404func TestSkippedManifestStepIsBuiltOnReplay(t *testing.T) {
1405 t.Parallel()
1406 h := newLandingHarness(t)
1407 h.flip(t)
1408 h.landAt = "after-verify"
1409 h.crashLand(t)
1410 j := h.landJournal(t)
1411 j.Mark(landStepFilesManifest, landingFilesManifestSkipped)
1412 if err := j.Commit(h.tdir); err != nil {
1413 t.Fatal(err)
1414 }
1415 if got := h.landJournal(t).Steps[landStepFilesManifest]; got != landingFilesManifestSkipped {
1416 t.Fatalf("the skip was not planted: %q", got)
1417 }
1418 h.freshTarget()
1419 h.land(t)
1420 h.installed(t)
1421 pm, m, _ := landedPreparedManifest(t, h)
1422 if pm.Op != p1ID || m.Seq != 0 {
1423 t.Fatalf("replay did not build the manifest: %+v seq %d", pm, m.Seq)
1424 }
1425}
1426
1427// A journalled files_manifest value that is not this install's binding
1428// (fix round 1, finding 3): empty, malformed, or a binding for another op,
1429// stamp or key. The install neither skips the build over it nor rebuilds
1430// over it: the replay refuses, the record stays at local, nothing is
1431// written under the files prefix and the journal keeps the value for an
1432// operator to see.
1433func TestLocalRefusesADamagedFilesManifestJournal(t *testing.T) {
1434 t.Parallel()
1435 good := func(h *landingHarness) preparedManifest {
1436 c := h.claim(t)
1437 return preparedManifest{Key: hostfiles.ManifestKey(movesFilesPrefix(c.Holder.Location, p1ID)), SHA256: sha([]byte("x")), Version: "v1", Op: p1ID, Stamp: c.Holder.Stamp}
1438 }
1439 for name, plant := range map[string]func(h *landingHarness) string{
1440 "empty": func(*landingHarness) string { return "" },
1441 "malformed": func(*landingHarness) string { return `{"key":` },
1442 "another op": func(h *landingHarness) string { pm := good(h); pm.Op = "mv_other"; return string(mustJSON(t, pm)) },
1443 "another stamp": func(h *landingHarness) string { pm := good(h); pm.Stamp++; return string(mustJSON(t, pm)) },
1444 "another key": func(h *landingHarness) string {
1445 pm := good(h)
1446 pm.Key = hostfiles.ManifestKey(movesFilesPrefix(h.claim(t).Holder.Location, "mv_other"))
1447 return string(mustJSON(t, pm))
1448 },
1449 "no version": func(h *landingHarness) string { pm := good(h); pm.Version = ""; return string(mustJSON(t, pm)) },
1450 } {
1451 t.Run(name, func(t *testing.T) {
1452 h := newLandingHarness(t)
1453 h.flip(t)
1454 h.landAt = "after-verify"
1455 h.crashLand(t)
1456 value := plant(h)
1457 j := h.landJournal(t)
1458 j.Mark(landStepFilesManifest, value)
1459 if err := j.Commit(h.tdir); err != nil {
1460 t.Fatal(err)
1461 }
1462 h.freshTarget()
1463 h.land(t)
1464 if m := h.read(t, p1Host).Move; m.LandingPhase != provision.LandLocal || m.Reason != "" {
1465 t.Fatalf("the record moved over a damaged journal: %+v", m)
1466 }
1467 j = h.landJournal(t)
1468 if got := j.Steps[landStepFilesManifest]; got != value {
1469 t.Fatalf("the journal value changed: %q", got)
1470 }
1471 if _, done := j.Steps[landStepFilesGen]; done {
1472 t.Fatal("the install went on past the damaged step")
1473 }
1474 for k := range h.f.buckets["data-uk"] {
1475 if strings.HasPrefix(k, cutRoot+"/files/") {
1476 t.Fatalf("written over a damaged journal: %s", k)
1477 }
1478 }
1479 })
1480 }
1481}
1482
1483// The placed tree or the placed env file vanishes after placement and
1484// before the manifest is built (fix round 1, finding 4): the install
1485// refuses rather than publishing an empty or partial image. The record
1486// stays at local, nothing is written under the files prefix and the step
1487// is not journalled.
1488func TestLocalRefusesAVanishedTreeBeforeTheManifest(t *testing.T) {
1489 t.Parallel()
1490 for name, vanish := range map[string]func(h *landingHarness) error{
1491 "the files directory": func(h *landingHarness) error { return os.RemoveAll(filepath.Join(h.tdir, p1Host)) },
1492 "the env file": func(h *landingHarness) error { return os.Remove(filepath.Join(h.tdir, p1Host+".env")) },
1493 } {
1494 t.Run(name, func(t *testing.T) {
1495 h := newLandingHarness(t)
1496 h.flip(t)
1497 h.landAt = "after-verify"
1498 h.crashLand(t)
1499 if err := vanish(h); err != nil {
1500 t.Fatal(err)
1501 }
1502 h.freshTarget()
1503 h.land(t)
1504 if m := h.read(t, p1Host).Move; m.LandingPhase != provision.LandLocal || m.Reason != "" {
1505 t.Fatalf("the record moved: %+v", m)
1506 }
1507 if _, ok := h.landJournal(t).Steps[landStepFilesManifest]; ok {
1508 t.Fatal("the step was journalled over a vanished input")
1509 }
1510 for k := range h.f.buckets["data-uk"] {
1511 if strings.HasPrefix(k, cutRoot+"/files/") {
1512 t.Fatalf("written over a vanished input: %s", k)
1513 }
1514 }
1515 })
1516 }
1517}
1518
1519// A cross-region landing's prepared manifest names destination-local
1520// objects it materialized from the sealed archive (claim P6-D3): every
1521// object lies under the manifest's own prefix in the target's bucket, and
1522// nothing is written under the source's files prefix.
1523func TestCrossRegionLandingManifestNamesDestinationObjects(t *testing.T) {
1524 t.Parallel()
1525 h := newLandingHarness(t)
1526 h.flip(t)
1527 h.land(t)
1528 h.installed(t)
1529 c := h.claim(t)
1530 pm, m, _ := landedPreparedManifest(t, h)
1531 prefix := movesFilesPrefix(c.Holder.Location, p1ID)
1532 if !strings.HasPrefix(pm.Key, prefix) {
1533 t.Fatalf("manifest key %s is not under %s", pm.Key, prefix)
1534 }
1535 for _, entries := range []map[string]hostfiles.Entry{m.Entries, m.Platform} {
1536 for name, e := range entries {
1537 if !strings.HasPrefix(e.Object, prefix+"objects/") {
1538 t.Fatalf("%s names %s, outside the destination prefix %s", name, e.Object, prefix)
1539 }
1540 obj, ok := h.f.buckets["data-uk"][e.Object]
1541 if !ok || sha([]byte(obj.body)) != e.SHA256 {
1542 t.Fatalf("%s names %s, which the target's bucket does not hold at that digest", name, e.Object)
1543 }
1544 }
1545 }
1546 for k := range h.f.buckets["data-ie"] {
1547 if strings.Contains(k, "/files/") {
1548 t.Fatalf("the landing wrote into the source region's files: %s", k)
1549 }
1550 }
1551}
1552
1553// A files manifest already at P that is not this install's (another
1554// writer's, or a damaged one) is not the landing's to replace: local
1555// refuses and the move rolls back (RollbackManifestOccupied), as it does
1556// for an occupied files directory.
1557func TestLocalRefusesAForeignPreparedManifest(t *testing.T) {
1558 t.Parallel()
1559 h := newLandingHarness(t)
1560 h.flip(t)
1561 c := h.claim(t)
1562 key := hostfiles.ManifestKey(movesFilesPrefix(c.Holder.Location, p1ID))
1563 body := `{"v":2,"seq":0,"entries":{"x.txt":{"object":"` + movesFilesPrefix(c.Holder.Location, p1ID) + `objects/0000000000000000-` + sha([]byte("x")) + `","sha256":"` + sha([]byte("x")) + `","size":1,"seq":0}},"platform":{}}`
1564 h.f.store("data-uk", key, fakeMoveObject{lease.MirrorObject{ETag: "foreign", Size: int64(len(body))}, body})
1565 h.f.setVersion("data-uk", key, h.f.nextVersion())
1566 h.land(t)
1567 if m := h.read(t, p1Host).Move; m.Reason != provision.RollbackManifestOccupied || m.LandingPhase != provision.LandLocal {
1568 t.Fatalf("not refused for %q at local: %+v", provision.RollbackManifestOccupied, m)
1569 }
1570 if got := h.f.buckets["data-uk"][key].body; got != body {
1571 t.Fatal("the foreign manifest was replaced")
1572 }
1573 if _, ok := h.landJournal(t).Steps[landStepFilesManifest]; ok {
1574 t.Fatal("the refused step was journalled as done")
1575 }
1576}
1577
1578// moveAuthority hands the landing's op to exec in the claim, as a
1579// replacement or a return of the executor would.
1580func moveAuthority(t *testing.T, h *landingHarness, exec string) {
1581 t.Helper()
1582 obs, err := hostclaim.Read(context.Background(), h.s, p1Host)
1583 if err != nil {
1584 t.Fatal(err)
1585 }
1586 if _, err := hostclaim.Update(context.Background(), h.s, obs, func(c *hostclaim.Claim) error {
1587 c.Op.Exec = exec
1588 if !slices.Contains(c.Op.Participants, exec) {
1589 c.Op.Participants = append(c.Op.Participants, exec)
1590 }
1591 return nil
1592 }); err != nil {
1593 t.Fatal(err)
1594 }
1595}
1596
1597// The prepared build re-checks the installation's authority (the op at
1598// phase local with this box executing, the holder at the captured stamp)
1599// immediately before every bucket write, retries included (fix round 1,
1600// finding 1; region HA's authorityStore). Authority moves to another
1601// executor between the first object upload and the next write: the stale
1602// writer writes nothing more, no manifest exists, and the step is not
1603// journalled. Once authority returns, the installer completes, reusing
1604// the one object the stale writer left.
1605func TestPreparedBuildRechecksAuthorityBeforeEveryWrite(t *testing.T) {
1606 t.Parallel()
1607 h := newLandingHarness(t)
1608 h.flip(t)
1609 c := h.claim(t)
1610 prefix := movesFilesPrefix(c.Holder.Location, p1ID)
1611 moved := false
1612 h.vs.deny = func(op, bucket, key string) error {
1613 if op == "put-if-absent" && strings.HasPrefix(key, prefix+"objects/") && !moved {
1614 moved = true
1615 moveAuthority(t, h, "other-box")
1616 }
1617 return nil
1618 }
1619 h.land(t)
1620 if !moved {
1621 t.Fatal("the build never uploaded an object")
1622 }
1623 objects := 0
1624 for k := range h.f.buckets["data-uk"] {
1625 if strings.HasPrefix(k, prefix+"objects/") {
1626 objects++
1627 }
1628 if k == hostfiles.ManifestKey(prefix) {
1629 t.Fatal("the stale writer published the manifest")
1630 }
1631 }
1632 if objects != 1 {
1633 t.Fatalf("%d objects after authority moved, want the one uploaded before it", objects)
1634 }
1635 if _, ok := h.landJournal(t).Steps[landStepFilesManifest]; ok {
1636 t.Fatal("the step was journalled without the manifest")
1637 }
1638 if m := h.read(t, p1Host).Move; m.LandingPhase != provision.LandLocal || m.Reason != "" {
1639 t.Fatalf("the record moved: %+v", m)
1640 }
1641 moveAuthority(t, h, "uk-box")
1642 h.vs.deny = nil
1643 h.freshTarget()
1644 h.land(t)
1645 h.installed(t)
1646 pm, m, _ := landedPreparedManifest(t, h)
1647 if pm.Op != p1ID || m.Seq != 0 {
1648 t.Fatalf("the authorized installer did not complete: %+v", pm)
1649 }
1650 vs, err := h.vs.ListVersions(context.Background(), "data-uk", prefix+"objects/")
1651 if err != nil {
1652 t.Fatal(err)
1653 }
1654 seen := map[string]int{}
1655 for _, v := range vs {
1656 seen[v.Key]++
1657 }
1658 for k, n := range seen {
1659 if n != 1 {
1660 t.Fatalf("%s has %d versions: the replay did not reuse it", k, n)
1661 }
1662 }
1663}
1664
1665// Nothing is written under the location's files prefix before the
1666// files_manifest step: the manifest follows the placed files, the env
1667// file, the timers and the columns (the step order of local, §7.6).
1668func TestLocalWritesNoFilesManifestBeforeItsStep(t *testing.T) {
1669 t.Parallel()
1670 h := newLandingHarness(t)
1671 h.flip(t)
1672 h.landAt = "after-verify"
1673 h.crashLand(t)
1674 for k := range h.f.buckets["data-uk"] {
1675 if strings.HasPrefix(k, cutRoot+"/files/") {
1676 t.Fatalf("a files manifest was written before the files_manifest step: %s", k)
1677 }
1678 }
1679}
1680
1681// Ruling G17: with the park engine's hooks unwired, as armPhase1 leaves
1682// them, the landing is not armed: the pass logs and writes nothing.
1683func TestLandingWaitsUnarmedInProduction(t *testing.T) {
1684 t.Parallel()
1685 h := newLandingHarness(t)
1686 h.flip(t)
1687 h.lw = &movesWorker{mirrors: map[string]lease.MirrorStore{}}
1688 c := h.landingCfg(t)
1689 c.ParkLeases = nil
1690 h.lw.cfg = c
1691 if landingArmed(c) {
1692 t.Fatal("armed without park leases")
1693 }
1694 if err := movesPass(context.Background(), h.s, h.lw); err != nil {
1695 t.Fatal(err)
1696 }
1697 if m := h.read(t, p1Host).Move; m.LandingPhase != provision.LandChecking {
1698 t.Fatalf("an unarmed landing advanced: %+v", m)
1699 }
1700 if _, err := hostguard.Capture(h.tdir, p1Host); !errors.Is(err, hostguard.ErrNoRecord) {
1701 t.Fatalf("an unarmed landing wrote the installation record: %v", err)
1702 }
1703 // Production's own wiring: everything but the park engine's hooks.
1704 var prod moveCfg
1705 hib := activator.Config{Bucket: "data-uk", Region: "eu-west-2", Prefix: "instances"}
1706 armPhase1(&prod, &movesWorker{mirrors: map[string]lease.MirrorStore{}}, hib, h.tdir, nil, nil, nil, hostwrite.Config{}, func(string) string { return "" })
1707 if prod.Guard.DataDir != h.tdir || prod.Local.DataDir != h.tdir || prod.OwnLease == nil || prod.RenderHeld == nil || prod.RemoveVerifyEntry == nil || prod.WriteGen == nil {
1708 t.Fatalf("production wiring: %+v", prod)
1709 }
1710 if prod.ParkLeases != nil || prod.ParkView != nil || prod.ParkAuthorize != nil || landingArmed(prod) {
1711 t.Fatal("production armed the landing")
1712 }
1713}
1714
1715// The generation classifier the stand-in view and parking share.
1716func TestLandingGenerationOf(t *testing.T) {
1717 loc := "instances/h"
1718 cases := map[string]struct {
1719 gen string
1720 ok bool
1721 }{
1722 loc + "/g-old/0000/x.ltx": {"old", true},
1723 loc + "/g-mv_1/install.json": {"mv_1", true},
1724 loc + "/files/g-mv_1/manifest": {"mv_1", true},
1725 loc + "/0000/x.ltx": {"", true},
1726 loc + "/0009/x.ltx": {"", true},
1727 loc + "/generation.json": {landingMarkerGeneration, true},
1728 loc + "/g-/x": {"", false},
1729 loc + "/stray.json": {"", false},
1730 loc + "/zzzz/x.ltx": {"", false},
1731 "instances/other/0000/x.ltx": {"", false},
1732 loc + "/" + strings.Repeat("a", 64) + "/0000/x.ltx": {"", false},
1733 }
1734 for key, want := range cases {
1735 gen, ok := landingGenerationOf(loc, key)
1736 if gen != want.gen || ok != want.ok {
1737 t.Errorf("%s: %q %v, want %q %v", key, gen, ok, want.gen, want.ok)
1738 }
1739 }
1740}
1741
1742// The target report pass keeps a landing's refusal and verdict live (G26).
1743func TestTargetLiveSetCoversTheCutAndTheLanding(t *testing.T) {
1744 t.Parallel()
1745 h := newLandingHarness(t)
1746 h.flip(t)
1747 w := &movesWorker{cfg: h.landingCfg(t), mirrors: map[string]lease.MirrorStore{}, archives: map[string]verifiedArchive{p1ID: {}, "mv_other": {}}}
1748 recs, err := provision.ListAllRecords(context.Background(), h.s)
1749 if err != nil {
1750 t.Fatal(err)
1751 }
1752 fleet, err := loadMoveFleet(context.Background(), h.s)
1753 if err != nil {
1754 t.Fatal(err)
1755 }
1756 w.targetReportPass(context.Background(), h.s, recs, fleet)
1757 if _, ok := w.archives[p1ID]; !ok {
1758 t.Fatal("the landing's verdict was evicted")
1759 }
1760 if _, ok := w.archives["mv_other"]; ok {
1761 t.Fatal("a finished move's verdict was kept")
1762 }
1763}
1764
1765// Fix round 1, item 1: the pass catches the claim up at restored before it
1766// returns. A crash after the record's restored CAS, or a following claim
1767// CAS that fails, leaves the claim behind; the next pass (a fresh worker)
1768// converges through movesPass alone.
1769func TestLandingRestoredCatchesTheClaimUp(t *testing.T) {
1770 t.Parallel()
1771 t.Run("crash after the record's restored", func(t *testing.T) {
1772 h := newLandingHarness(t)
1773 h.flip(t)
1774 h.landAt = "after-record-restored"
1775 h.crashLand(t)
1776 if c := h.claim(t); c.Op.Phase != provision.LandConfigured {
1777 t.Fatalf("fixture: claim %+v", c.Op)
1778 }
1779 h.freshTarget()
1780 h.land(t)
1781 h.installed(t)
1782 })
1783 t.Run("the following claim CAS fails", func(t *testing.T) {
1784 h := newLandingHarness(t)
1785 h.flip(t)
1786 // At the boundary the claim is moved two steps back, so the mirror's
1787 // SetMovePhase(configured, restored) is refused and the claim stays
1788 // behind; the pass must not fail, and the next pass walks it up.
1789 h.landFault = func(point string) error {
1790 if point == "after-record-restored" {
1791 rewriteClaimOp(t, h.s, func(op *hostclaim.Op) { op.Phase = provision.LandBuilding })
1792 }
1793 return nil
1794 }
1795 h.land(t)
1796 h.landFault = nil
1797 if !containsString(h.landHits, "after-record-restored") {
1798 t.Fatalf("the boundary was never reached: %v", h.landHits)
1799 }
1800 // The refused mirror is logged, and the same pass's restored step
1801 // walks the claim from building to restored before it returns.
1802 h.installed(t)
1803 h.freshTarget()
1804 h.land(t)
1805 h.installed(t)
1806 })
1807}
1808
1809// Fix round 1, item 2: a build that does not verify marks the rollback and
1810// leaves the phase at configured: no transition to restored, no digest, no
1811// database installed, no marker.
1812func TestLandingCorruptBuildMarksRollback(t *testing.T) {
1813 t.Parallel()
1814 h := newLandingHarness(t)
1815 h.flip(t)
1816 h.landFault = func(point string) error {
1817 if point == "before-db-verify" {
1818 f, err := os.OpenFile(filepath.Join(h.tdir, ".moves", p1Host, p1ID, "build", p1Host+".db"), os.O_APPEND|os.O_WRONLY, 0o600)
1819 if err != nil {
1820 t.Fatal(err)
1821 }
1822 if _, err := f.Write([]byte("corrupt")); err != nil {
1823 t.Fatal(err)
1824 }
1825 f.Close()
1826 }
1827 return nil
1828 }
1829 h.land(t)
1830 if !containsString(h.landHits, "before-db-verify") {
1831 t.Fatalf("the verify point was never reached: %v", h.landHits)
1832 }
1833 m := h.read(t, p1Host).Move
1834 if m.Reason != provision.RollbackInstallMismatch || m.LandingPhase != provision.LandConfigured || m.State != provision.MoveLanding {
1835 t.Fatalf("after the corrupt build: %+v", m)
1836 }
1837 if m.Landing != nil && m.Landing.InstalledDBSHA256 != "" {
1838 t.Fatalf("a digest was recorded: %+v", m.Landing)
1839 }
1840 dbPath := provision.DefaultDBPath(h.tdir, p1Host)
1841 for _, p := range []string{dbPath, dbPath + ".gen"} {
1842 if _, err := os.Stat(p); !errors.Is(err, os.ErrNotExist) {
1843 t.Fatalf("%s exists after a refused install: %v", p, err)
1844 }
1845 }
1846 if c := h.claim(t); c.Op.Phase != provision.LandConfigured {
1847 t.Fatalf("claim: %+v", c.Op)
1848 }
1849}
1850
1851// Fix round 1, item 3: the generation is written only under live lease
1852// authority. A rebuild (the scratch lost after published released the
1853// lease) takes the location's lease first, and waits while another holder
1854// has it, writing nothing; a lease lost during the build stops the build
1855// at the next generation write, and the replay completes.
1856func TestLandingRebuildNeedsTheLease(t *testing.T) {
1857 t.Parallel()
1858 ctx := context.Background()
1859 h := newLandingHarness(t)
1860 h.flip(t)
1861 h.landAt = "after-record-configured"
1862 h.crashLand(t)
1863 if err := os.RemoveAll(filepath.Join(h.tdir, ".moves", p1Host, p1ID, "build")); err != nil {
1864 t.Fatal(err)
1865 }
1866 h.freshTarget()
1867 other, err := h.leases.Acquire(ctx, "data-uk", cutRoot)
1868 if err != nil {
1869 t.Fatal(err)
1870 }
1871 prefix := moves.GenerationPrefix(cutRoot, p1ID)
1872 before := len(h.vs.callsOn(prefix))
1873 h.land(t)
1874 if m := h.read(t, p1Host).Move; m.LandingPhase != provision.LandConfigured || m.Reason != "" {
1875 t.Fatalf("installed without the lease: %+v", m)
1876 }
1877 if got := h.vs.callsOn(prefix); len(got) != before {
1878 t.Fatalf("generation requests while another holder had the lease: %v", got[before:])
1879 }
1880 if _, err := os.Stat(provision.DefaultDBPath(h.tdir, p1Host)); !errors.Is(err, os.ErrNotExist) {
1881 t.Fatalf("a database was installed without the lease: %v", err)
1882 }
1883 if err := other.Release(ctx); err != nil {
1884 t.Fatal(err)
1885 }
1886 h.land(t)
1887 h.installed(t)
1888}
1889
1890func TestLandingBuildStopsWhenTheLeaseIsLost(t *testing.T) {
1891 t.Parallel()
1892 h := newLandingHarness(t)
1893 h.flip(t)
1894 prefix := moves.GenerationPrefix(cutRoot, p1ID)
1895 lost := false
1896 h.vs.deny = func(op, bucket, key string) error {
1897 if op == "put-if-absent" && strings.HasPrefix(key, prefix) && !lost {
1898 lost = true
1899 h.leases.expire() // the holder went dark between two generation writes
1900 }
1901 return nil
1902 }
1903 h.land(t)
1904 h.vs.deny = nil
1905 if !lost {
1906 t.Fatal("the build never wrote")
1907 }
1908 if m := h.read(t, p1Host).Move; m.LandingPhase != provision.LandBuilding {
1909 t.Fatalf("the build completed on a lost lease: %+v", m)
1910 }
1911 if _, ok := h.f.buckets["data-uk"][moves.InstallManifestKey(prefix)]; ok {
1912 t.Fatal("install.json was written on a lost lease")
1913 }
1914 var written []string
1915 for k := range h.f.buckets["data-uk"] {
1916 if strings.HasPrefix(k, prefix) {
1917 written = append(written, strings.TrimPrefix(k, prefix))
1918 }
1919 }
1920 if len(written) > 1 {
1921 t.Fatalf("writes after the lease was lost: %v", written)
1922 }
1923 h.freshTarget()
1924 h.land(t)
1925 h.installed(t)
1926}
1927
1928// Fix round 1, item 4: landing_deadline is checked again right before the
1929// install transition. Past it after the release gate, the rollback is marked
1930// and nothing is installed; past it after the install itself, the rollback
1931// is marked and restored is never written.
1932func TestLandingDeadlineBeforeTheInstallTransition(t *testing.T) {
1933 t.Parallel()
1934 for _, point := range []string{"after-release-ready", "after-db-rename"} {
1935 t.Run(point, func(t *testing.T) {
1936 h := newLandingHarness(t)
1937 h.flip(t)
1938 h.landFault = func(p string) error {
1939 if p == point {
1940 h.clock.Advance(cutLandingWindow + time.Minute)
1941 }
1942 return nil
1943 }
1944 h.land(t)
1945 m := h.read(t, p1Host).Move
1946 if m.Reason != provision.RollbackLandingDeadline || m.LandingPhase != provision.LandConfigured {
1947 t.Fatalf("past the deadline at %s: %+v", point, m)
1948 }
1949 if m.Landing != nil && m.Landing.InstalledDBSHA256 != "" {
1950 t.Fatalf("a digest was recorded: %+v", m.Landing)
1951 }
1952 _, err := os.Stat(provision.DefaultDBPath(h.tdir, p1Host))
1953 if point == "after-release-ready" && !errors.Is(err, os.ErrNotExist) {
1954 t.Fatalf("installed past the deadline: %v", err)
1955 }
1956 if c := h.claim(t); c.Op.Phase != provision.LandConfigured {
1957 t.Fatalf("claim: %+v", c.Op)
1958 }
1959 })
1960 }
1961}
1962
1963// retryingStore is a versioned store whose first generation write blocks
1964// like a client retrying a request: the lease lapses while it is in flight,
1965// and if nothing cancels the request its retry lands after the loss.
1966type retryingStore struct {
1967 lease.VersionedStore
1968 h *landingHarness
1969 prefix string
1970 once sync.Once
1971 landed bool
1972 blocked bool
1973}
1974
1975func (r *retryingStore) PutIfAbsent(ctx context.Context, bucket, key string, body []byte) (string, error) {
1976 block := false
1977 if strings.HasPrefix(key, r.prefix) {
1978 r.once.Do(func() { block = true })
1979 }
1980 if !block {
1981 return r.VersionedStore.PutIfAbsent(ctx, bucket, key, body)
1982 }
1983 r.blocked = true
1984 r.h.leases.expire() // the holder goes dark while the request is in flight
1985 select {
1986 case <-ctx.Done():
1987 return "", ctx.Err()
1988 case <-time.After(1500 * time.Millisecond):
1989 r.landed = true // the client's retry succeeds after the lease was lost
1990 return r.VersionedStore.PutIfAbsent(ctx, bucket, key, body)
1991 }
1992}
1993
1994// Fix round 2: every generation request runs under a context derived from
1995// the lease's, so a lease lost while a request is in flight cancels it and
1996// its retries; nothing is recorded. The replay builds afresh.
1997func TestLandingBuildIsCancelledWhenTheLeaseIsLostMidRequest(t *testing.T) {
1998 t.Parallel()
1999 h := newLandingHarness(t)
2000 h.flip(t)
2001 prefix := moves.GenerationPrefix(cutRoot, p1ID)
2002 rs := &retryingStore{VersionedStore: h.vs, h: h, prefix: prefix}
2003 h.store = rs
2004 h.land(t)
2005 h.store = nil
2006 if !rs.blocked {
2007 t.Fatal("the build never wrote under the generation")
2008 }
2009 if rs.landed {
2010 t.Fatal("the request landed after the lease was lost: it was not cancelled")
2011 }
2012 if m := h.read(t, p1Host).Move; m.LandingPhase != provision.LandBuilding {
2013 t.Fatalf("record: %+v", m)
2014 }
2015 for k := range h.f.buckets["data-uk"] {
2016 if strings.HasPrefix(k, prefix) {
2017 t.Fatalf("written on a lost lease: %s", k)
2018 }
2019 }
2020 if j := h.landJournal(t); j.Done(landStepInstall) {
2021 t.Fatal("the build was recorded on a lost lease")
2022 }
2023 h.freshTarget()
2024 h.land(t)
2025 h.installed(t)
2026}
2027
2028// openState is the record's state, the claim's phase and the row's status
2029// at a step of the open.
2030func (h *landingHarness) openState(t *testing.T) string {
2031 return h.read(t, p1Host).Move.State + "/" + h.claim(t).Op.Phase + "/" + h.route.Status
2032}
2033
2034// errHoldProbed is holdProbed's stop before the commit.
2035var errHoldProbed = errors.New("held at probed by the test")
2036