carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package host
2
3import (
4 "context"
5 "encoding/json"
6 "errors"
7 "fmt"
8 "log"
9 "os"
10 "path/filepath"
11 "strconv"
12 "time"
13
14 "github.com/carlosframework/platform/internal/hostclaim"
15 "github.com/carlosframework/platform/internal/hostguard"
16 "github.com/carlosframework/platform/internal/hostwrite"
17 "github.com/carlosframework/platform/internal/moves"
18 "github.com/carlosframework/platform/internal/provision"
19 "github.com/carlosframework/platform/internal/registry"
20 "github.com/carlosframework/platform/internal/release"
21)
22
23// The landing on the target (moves spec §5.1, §7.5, §7.6), behind
24// CARLOS_MOVES_PHASE1. After the flip the record's region is the target's
25// and reconcile has minted the held row, so the target's pass visits the
26// record (OwnsRoute) and phase1MoveRound dispatches it here (ruling G34).
27// One pass drives one landing record as far as it can, as the cut does:
28// each phase does its work, then advances landing_phase by one guarded CAS,
29// then mirrors the step into the claim's op.phase (§7.5: the record is the
30// decision, the claim follows it). Nothing is carried in memory between
31// passes but the own-location lease (building through published): every
32// phase resumes from the record, the claim, this box's own bucket and the
33// op journal (moves.Journal, <DataDir>/.moves/<host>/<op>/). checking is
34// the landing's own; parking to restored run in the generation installer
35// the dirty return shares (movesinstall.go, Plan 4 Task 3), bound to this
36// landing by installSource.
37//
38// Phase names (ruling G8, the cut's convention). A record at X means every
39// phase before X is complete and X's own work may have begun; a replay of X
40// redoes X's work from durable state, and each phase's comment says what it
41// finds. The one name read the other way is `restored`: it is written by
42// the CAS that ends `configured`, after the database is installed and its
43// markers written, stamping landing.installed_db_sha256, so Plan 3's probe
44// reads `restored` as "the install is complete". Every later phase is the
45// probe's (Plan 3).
46//
47// The claim never leads the record. Every phase starts with syncClaimPhase:
48// equal is the common case; a claim behind the record (a crash between the
49// record CAS and the claim CAS) is walked forward along LandingOrder with
50// one claim CAS per adjacent step; a claim ahead is left alone and logged, and the phase's own
51// RequireMoveOp then refuses, so nothing is written over it.
52//
53// Nothing here ever serves from moves/: the lane is read by exact version
54// into the generation the build writes and into the files directory the
55// archive is extracted into (M14, M15). A failure that is permanent for this
56// landing is a rollback (§7.8), which Plan 4 builds: until then the record
57// carries the reason (provision.MoveMarkRollback) and this pass leaves a
58// marked record alone. The landing's deadline passing before `restored`
59// marks it too: the earlier of the cut's absolute cut_deadline and the
60// flip's landing_deadline (spec §7.4; landingDeadlineOf).
61//
62// Local installation (final review, findings 1 and 2). Every destructive
63// local mutation (the files and env install, .files-gen, the database
64// rename and <dbPath>.gen) runs inside the host guard at the installation
65// stamp captured from the record (landingRun.guarded, the pattern of
66// hostbox ColdRestore.Install): the claim and the record are read fresh
67// there and must still be this landing at this phase, and the destination
68// is checked again there. Retirement (park.Local) takes the guard itself
69// and runs outside those blocks. The landing never deletes at the live
70// paths: the archive is extracted into the op's own staging and moved into
71// place only when nothing it did not write is there.
72//
73// Arming (ruling G17; Plan 5 Task 9a). armPhase1 wires the hooks that exist
74// on main (the guard, park.Local, the own-location lease, RenderHeld, the
75// registry setters, the restore-verification record, the marker writer).
76// ParkLeases, ParkView, ParkAuthorize and ParkLocalAuthorize are the
77// claim's (MoveParks), and ParkRunHeld is park.Engine.RunHeld over the
78// claim's leases (M5), all set by armMovesParks only once the wake-source
79// gate opens (movesWakeSourceRegistered, Plan 5 Task 9b); nothing stands in
80// for them. landingArmed therefore answers false in production and the pass
81// logs a standing notice instead of landing; the tests arm everything with
82// fakes (movesparkfakes_test.go).
83// Under
84// CARLOS_HOST_CLAIM the landing's held writes select their Work from the
85// claim (Task 7, Q2): the timers as held-timers, the render as held-config
86// and the release as held-release, which the configured step now publishes
87// (T7 only stages under the claim; Paul, 2026-10-12). hostbox.Policy.Moves,
88// MovesClaimPolicy (movespolicy.go), decides them. The landing still refuses
89// at checking with a recorded reason (ruling G16, RollbackClaimWired) until
90// the wake-source gate opens (movesWakeSourceRegistered), and lands after.
91
92// landingStepsPerPass bounds the phases one pass runs.
93const landingStepsPerPass = 8
94
95// landingRequestLimit is the residue parks' request limit: the longest a
96// request against the location runs (hostbox.RequestLimit, the cut's too).
97const landingRequestLimit = cutRequestLimit
98
99// Op journal steps the landing writes (moves.Journal). Each sub-step of
100// `local` is a step, so a replay skips what is durable and finishes the
101// rest; `.files-gen` is written last and vouches for them all (§7.6).
102const (
103 landStepPrevious = "previous" // the earlier stay's stamp S' (decimal), noted before Activate
104 landStepResidue = "residue" // the generations whose residue parks were queued (JSON list)
105 landStepInstall = "install" // the completed install manifest (JSON)
106 landStepPublished = "published" // the build is complete and recorded (R10-3: no generation.json)
107 landStepRetired = "retired" // the earlier stay's artifacts are parked, or there were none
108 landStepStaged = "staged" // the archive is extracted into <JournalDir>/extract/ (JSON stagedFiles)
109 landStepFilesPlaced = "files_placed" // the install receipt: the staged tree is at the live paths (JSON stagedFiles)
110 landStepExtracted = "extracted" // the archive is in the files directory and <host>.env
111 landStepInventory = "inventory" // the placed files and env are registered under F
112 landStepTimers = "timers" // the timers table is the sealed snapshot
113 landStepColumns = "columns" // replication and next_due carried over
114 landStepVerify = "verify" // the restore-verification entry is gone
115 landStepFilesManifest = "files_manifest" // the prepared files manifest's binding (JSON preparedManifest: key, sha256, op, stamp; C8, Task 6)
116 landStepFilesGen = "files_gen" // .files-gen written
117 landStepPreInstall = "pre_install" // the database's siblings absent before its install (JSON preInstall)
118 landStepInventoryDB = "inventory_db" // the installed database and its marker are registered under F (carry (b))
119 landStepPreRender = "pre_render" // the held render's ownership before its write (JSON preRender, carry (c))
120 landStepRendered = "rendered" // the held render's file, "sha256:<hex>", journalled even when its ack failed (carry (c))
121)
122
123// landingFilesManifestSkipped is what the files_manifest step recorded
124// before C8 (the prepared-manifest build, Task 6) existed. Nothing writes
125// it now; a journal that holds it counts as not done (filesManifestBuilt),
126// so a replay builds the manifest (ruling Q3 item 2).
127const landingFilesManifestSkipped = "skipped: the prepared files manifest needs C8 (files.BuildPrepared)"
128
129// landingRun is one pass's state for one landing record.
130type landingRun struct {
131 w *movesWorker
132 s release.Store
133 rec provision.Record
134 g provision.MoveGuard
135 guard hostguard.Guard
136}
137
138// parkHooksArmed reports whether the claim's three park hooks are wired:
139// the leases, the view and the authorizer (M17). Every moves arm requires
140// all three; nothing stands in for a missing one.
141func parkHooksArmed(cfg moveCfg) bool {
142 return cfg.ParkLeases != nil && cfg.ParkView != nil && cfg.ParkAuthorize != nil
143}
144
145// landingArmed reports whether this box can run the landing: every hook
146// the phases call is wired, the claim's three park hooks, RunHeld (M5: the
147// residue parks run under the landing's own lease) and the claim's local
148// authorizer (M9: the install's park.Local) among them. In production
149// armMovesParks sets those only once the wake-source gate opens
150// (movesWakeSourceRegistered, Plan 5 Task 9b), so this answers false there.
151func landingArmed(cfg moveCfg) bool {
152 return cfg.Box != "" && cfg.Guard.DataDir != "" && cfg.Local.DataDir != "" && parkHooksArmed(cfg) && cfg.ParkRunHeld != nil && cfg.ParkLocalAuthorize != nil &&
153 cfg.OwnLease != nil && cfg.RenderHeld != nil && cfg.ReplaceTimers != nil && cfg.SetReplication != nil &&
154 cfg.SetNextDue != nil && cfg.RemoveVerifyEntry != nil && cfg.WriteGen != nil && cfg.Route != nil &&
155 cfg.FilesDir != nil && cfg.Versioned != nil && cfg.ReleaseReady != nil && cfg.SecretsPriv != nil
156}
157
158// landingPass is the MoveLanding arm of phase1MoveRound.
159func (w *movesWorker) landingPass(ctx context.Context, s release.Store, rec provision.Record) error {
160 cfg := w.cfg
161 // The rollback decision (movesrollback.go, J10, J11): a recorded mark
162 // or the landing's deadline, at any phase, armed or not. A landing the
163 // decision took is abandoned, and with it the location's lease a build
164 // keeps across passes (building through published): it is released
165 // here, before the pass ends, or the target's cleaning could never take
166 // the same lease (the final review's finding 2).
167 if done, err := w.rollBackLanding(ctx, s, rec); done || err != nil {
168 if done {
169 releaseOwnLease(w, rec.Host)
170 }
171 return err
172 }
173 if !landingArmed(cfg) {
174 w.notice(rec.Host+"#landing", "moves: %s: the landing of %s waits: this box's landing is not armed (the claim's park hooks are set from MoveParks only once this box's moves wake source is registered, Plan 5 Task 9b)", rec.Host, rec.Move.ID)
175 return nil
176 }
177 l := &landingRun{w: w, s: s, rec: rec, g: provision.MoveGuard{ID: rec.Move.ID, Incarnation: rec.Move.Incarnation}}
178 l.guard = cfg.Guard
179 if l.guard.Claims == nil {
180 l.guard.Claims = hostguard.StoreReader{Store: s}
181 }
182 defer l.settleLease()
183 for i := 0; i < landingStepsPerPass; i++ {
184 advanced, err := l.step(ctx)
185 if err != nil || !advanced {
186 return err
187 }
188 fresh, err := provision.GetRecord(ctx, s, rec.Account, rec.App, rec.Host)
189 if err != nil {
190 return fmt.Errorf("re-read the record: %w", err)
191 }
192 if fresh.Move == nil || fresh.Move.ID != l.g.ID || fresh.Move.Incarnation != l.g.Incarnation || fresh.Move.State != provision.MoveLanding {
193 return nil
194 }
195 l.rec = *fresh
196 }
197 return nil
198}
199
200func (l *landingRun) cfg() moveCfg { return l.w.cfg }
201
202// fault is the test seam after a side effect.
203func (l *landingRun) fault(point string) error {
204 if f := l.w.cfg.landFault; f != nil {
205 return f(point)
206 }
207 return nil
208}
209
210// step runs the record's current phase once. advanced is true when it
211// wrote the record (a phase or the rollback marker).
212func (l *landingRun) step(ctx context.Context) (bool, error) {
213 m := l.rec.Move
214 if m.State != provision.MoveLanding {
215 return false, nil
216 }
217 if m.Reason != "" {
218 // Marked: the next pass's rollback decision (rollBackLanding) rolls
219 // it back; markRollback nudged the serial for it.
220 return false, nil
221 }
222 // Every phase checks the landing's deadline at entry: the install's
223 // phases (P2-deadline), and restored, probing and probed (H8: the
224 // probe works to the same deadline).
225 if l.pastDeadline() {
226 return l.markRollback(ctx, provision.RollbackLandingDeadline)
227 }
228 switch m.LandingPhase {
229 case provision.LandChecking:
230 return l.checking(ctx)
231 case provision.LandParking, provision.LandBuilding, provision.LandPublished, provision.LandLocal, provision.LandConfigured:
232 // parking to restored are the shared installer's (movesinstall.go).
233 advanced, _, err := installGeneration(ctx, l.s, l.w, l.rec, l.installSource())
234 return advanced, err
235 case provision.LandRestored:
236 // The install is complete. The claim half of the last CAS first: a
237 // crash after the record's restored, or a mirror that failed, leaves
238 // the claim behind (fix round 1, item 1). Then the probe begins,
239 // when this box has a probe wake (unarmed in production).
240 if _, err := l.syncClaimPhase(ctx); err != nil {
241 return false, err
242 }
243 if l.cfg().Probe == nil {
244 l.w.notice(l.rec.Host+"#probe", "moves: %s: landing %s waits at restored: this box has no probe wake (claim Plan 11's launcher and Plan 5 Task 9's arming are needed)", l.rec.Host, l.g.ID)
245 return false, nil
246 }
247 return l.advance(ctx, provision.LandRestored, provision.LandProbing, nil)
248 case provision.LandProbing:
249 return l.probing(ctx)
250 case provision.LandProbed:
251 // The probe passed: the commit (moveslandcommit.go).
252 return l.probed(ctx)
253 }
254 return false, nil
255}
256
257// landingDeadlineOf is the deadline a landing before restored works to:
258// provision.LandingDeadlineOf, the earlier of the cut's absolute deadline
259// and landing_deadline, which the move's timings share (Plan 6 preflight
260// L20). which names the one that applies, for the log.
261func landingDeadlineOf(m *provision.MoveState) (d *time.Time, which string) {
262 return provision.LandingDeadlineOf(m)
263}
264
265// pastDeadline reports whether the landing's deadline (landingDeadlineOf)
266// has passed now. It is checked at every phase's entry (step), and in
267// configured again before the install and before the transition that
268// names it complete.
269func (l *landingRun) pastDeadline() bool {
270 d, which := landingDeadlineOf(l.rec.Move)
271 if d == nil || !l.cfg().Now().After(*d) {
272 return false
273 }
274 log.Printf("moves: %s: landing %s is past its %s (%s)", l.rec.Host, l.g.ID, which, d.UTC().Format(time.RFC3339))
275 return true
276}
277
278// landingBefore reports whether phase comes before other in LandingOrder.
279func landingBefore(phase, other string) bool {
280 order := provision.LandingOrder()
281 return landingIndex(order, phase) >= 0 && landingIndex(order, phase) < landingIndex(order, other)
282}
283
284func landingIndex(order []string, phase string) int {
285 for i, p := range order {
286 if p == phase {
287 return i
288 }
289 }
290 return -1
291}
292
293// transition writes one guarded record transition. A conflict is someone
294// else's decision (a rollback, a console write): logged, and the pass ends
295// without error.
296func (l *landingRun) transition(ctx context.Context, what string, mut func(*provision.Record) error) (bool, error) {
297 var pre provision.Record
298 after, err := provision.SetRecordMove(ctx, l.s, l.rec.Account, l.rec.App, l.rec.Host, observedMove(&pre, mut))
299 if err != nil {
300 if errors.Is(err, provision.ErrMoveConflict) {
301 log.Printf("moves: %s: %s of %s refused (%v); the record moved on", l.rec.Host, what, l.g.ID, err)
302 return false, nil
303 }
304 return false, fmt.Errorf("%s: %w", what, err)
305 }
306 l.w.logTransition(ctx, l.s, pre, *after)
307 return true, nil
308}
309
310// advance writes the record's step and then mirrors it into the claim. A
311// mirror that fails is logged: the next pass's syncClaimPhase writes it.
312func (l *landingRun) advance(ctx context.Context, from, to string, stamp func(*provision.MoveState)) (bool, error) {
313 ok, err := l.transition(ctx, "the landing "+from+" to "+to, provision.MoveLandingPhase(l.g, from, to, stamp))
314 if !ok {
315 return ok, err
316 }
317 log.Printf("moves: %s: landing %s is %s", l.rec.Host, l.g.ID, to)
318 if err := l.fault("after-record-" + to); err != nil {
319 return true, err
320 }
321 if err := l.mirrorPhase(ctx, from, to); err != nil {
322 log.Printf("moves: %s: the claim half of landing %s waits for the next pass: %v", l.rec.Host, to, err)
323 }
324 return true, nil
325}
326
327// mirrorPhase is the claim CAS that follows a record step (§7.5).
328func (l *landingRun) mirrorPhase(ctx context.Context, from, to string) error {
329 obs, err := l.claim(ctx)
330 if err != nil {
331 return err
332 }
333 step := hostclaim.SetMovePhase(l.g.ID, hostclaim.KindLanding, from, to)
334 if hostclaim.SessionPhase(hostclaim.KindLanding, from) && hostclaim.SessionPhase(hostclaim.KindLanding, to) {
335 // probing to probed (and Task 7's committed): the admitted probe
336 // session is the serving session, so the CAS must keep it (H3).
337 step = hostclaim.KeepWriterSession(l.g.ID, step)
338 }
339 _, err = hostclaim.Update(ctx, l.s, obs, step)
340 return err
341}
342
343// markRollback records why the landing stopped (§7.6; Plan 4's rollback
344// transition replaces this one call site).
345func (l *landingRun) markRollback(ctx context.Context, reason string) (bool, error) {
346 ok, err := l.transition(ctx, "the rollback marker", provision.MoveMarkRollback(l.g, reason))
347 if ok {
348 log.Printf("moves: %s: landing %s stops for a rollback: %s", l.rec.Host, l.g.ID, reason)
349 nudgeSerial(ctx, l.s)
350 }
351 return ok, err
352}
353
354// claim reads the host claim and checks it is this move's op for the
355// record's incarnation.
356func (l *landingRun) claim(ctx context.Context) (hostclaim.Observed, error) {
357 obs, err := hostclaim.Read(ctx, l.s, l.rec.Host)
358 if err != nil {
359 return obs, fmt.Errorf("read the host claim: %w", err)
360 }
361 if obs.Absent {
362 return obs, fmt.Errorf("move %s has no host claim", l.g.ID)
363 }
364 if err := hostclaim.RequireOp(l.g.ID)(obs.Claim); err != nil {
365 return obs, fmt.Errorf("move %s: %w", l.g.ID, err)
366 }
367 if h := obs.Claim.Holder; h == nil || h.Incarnation != l.rec.Move.Incarnation {
368 return obs, fmt.Errorf("move %s: the host claim names another incarnation", l.g.ID)
369 }
370 return obs, nil
371}
372
373// syncClaimPhase is every phase's first step (§7.5): the claim's op.phase
374// follows the record's landing_phase. Behind, it is walked forward one step
375// at a time, each in its own CAS; ahead, it is left alone and logged (the phase's own
376// RequireMoveOp then refuses); equal is the common case. The observation
377// returned is the one the phase works from, and names this box as the
378// executor.
379func (l *landingRun) syncClaimPhase(ctx context.Context) (hostclaim.Observed, error) {
380 cfg := l.cfg()
381 obs, err := l.claim(ctx)
382 if err != nil {
383 return obs, err
384 }
385 op := obs.Claim.Op
386 if op.Kind != hostclaim.KindLanding {
387 return obs, fmt.Errorf("move %s: the claim is %s/%q, not landing", l.g.ID, op.Kind, op.Phase)
388 }
389 if cfg.Box == "" || op.Exec != cfg.Box {
390 return obs, fmt.Errorf("move %s: the claim's executor is %q, not this box", l.g.ID, op.Exec)
391 }
392 order := provision.LandingOrder()
393 ci, ri := landingIndex(order, op.Phase), landingIndex(order, l.rec.Move.LandingPhase)
394 switch {
395 case ci < 0 || ri < 0:
396 return obs, fmt.Errorf("move %s: landing phases %q (claim) and %q (record) are not both known", l.g.ID, op.Phase, l.rec.Move.LandingPhase)
397 case ci == ri:
398 return obs, nil
399 case ci > ri:
400 log.Printf("moves: %s: the claim's landing phase %q is ahead of the record's %q; left alone", l.rec.Host, op.Phase, l.rec.Move.LandingPhase)
401 return obs, nil
402 }
403 for i := ci; i < ri; i++ {
404 step := hostclaim.SetMovePhase(l.g.ID, hostclaim.KindLanding, order[i], order[i+1])
405 if hostclaim.SessionPhase(hostclaim.KindLanding, order[i]) && hostclaim.SessionPhase(hostclaim.KindLanding, order[i+1]) {
406 step = hostclaim.KeepWriterSession(l.g.ID, step)
407 }
408 obs, err = hostclaim.Update(ctx, l.s, obs, step)
409 if err != nil {
410 return obs, fmt.Errorf("catch the claim up to %s: %w", l.rec.Move.LandingPhase, err)
411 }
412 }
413 log.Printf("moves: %s: the claim's landing phase caught up from %q to %q", l.rec.Host, op.Phase, l.rec.Move.LandingPhase)
414 return obs, nil
415}
416
417// requirePhase is the phase's K5 revalidation on the synced observation.
418func (l *landingRun) requirePhase(obs hostclaim.Observed, phase string) error {
419 if err := hostclaim.RequireMoveOp(l.g.ID, hostclaim.KindLanding, phase)(obs.Claim); err != nil {
420 return fmt.Errorf("move %s: %w", l.g.ID, err)
421 }
422 return nil
423}
424
425// landingCheck is the claim check the guard runs before a local write of
426// phase: this landing's op, at that phase, with this box executing.
427func (l *landingRun) landingCheck(phase string) func(*hostclaim.Claim) error {
428 return func(c *hostclaim.Claim) error {
429 if err := hostclaim.RequireMoveOp(l.g.ID, hostclaim.KindLanding, phase)(c); err != nil {
430 return err
431 }
432 if c.Op.Exec != l.cfg().Box {
433 return fmt.Errorf("%w: the executor is %q, not this box", hostclaim.ErrWrongState, c.Op.Exec)
434 }
435 return nil
436 }
437}
438
439// route is the host's registry row on this box.
440func (l *landingRun) route() (registry.Route, error) {
441 rt, err := l.cfg().Route(l.rec.Host)
442 if err != nil {
443 return registry.Route{}, fmt.Errorf("read the registry row: %w", err)
444 }
445 if rt == nil {
446 return registry.Route{}, fmt.Errorf("%s has no registry row on this box", l.rec.Host)
447 }
448 return *rt, nil
449}
450
451func (l *landingRun) dbPath() string { return provision.DefaultDBPath(l.cfg().DataDir, l.rec.Host) }
452
453func (l *landingRun) journal() (*moves.Journal, error) {
454 return moves.ReadJournal(l.cfg().DataDir, l.rec.Host, l.g.ID)
455}
456
457// ensureLocalDirs creates the two directories the host guard and park.Local
458// require under the data directory (hostguard/ for the installation record,
459// .parked/ for retired artifacts). On a box with claims on they exist
460// already; a Phase 1 landing on a box without them creates them.
461func (l *landingRun) ensureLocalDirs() error {
462 for _, d := range []string{"hostguard", ".parked"} {
463 if err := os.MkdirAll(filepath.Join(l.cfg().DataDir, d), 0o770); err != nil {
464 return err
465 }
466 }
467 return nil
468}
469
470// checking (§7.6): the refusals this box can know (G16, G11), the flip's
471// claim half if the source crashed before it (G12), then the claim's word:
472// landing/checking for this move with this box executing and a root-layout
473// holder; the source's complete stop evidence; this box in touched.boxes
474// (K12, before any local write); move.json's incarnation; the earlier
475// stay's stamp S' noted in the journal before the installation record is
476// activated at F (holder.stamp, the flip's); and a park-local queued only
477// when the inventory has paths tagged S' (ruling G5). No lease is taken
478// here: the residue parks take the location's lease themselves.
479//
480// A replay finds checking and runs it all again: every write is
481// idempotent (the flip, Touch and the park queue under the same op, the
482// journal step written once, Activate at the same stamp), and Capture after
483// Activate reads F, which is never noted as a previous stay.
484func (l *landingRun) checking(ctx context.Context) (bool, error) {
485 cfg := l.cfg()
486 host, id := l.rec.Host, l.g.ID
487 // Ruling G16, as Task 7 settles it (Q2.3): with host claims on, the
488 // landing's held writes run under the claim's selections (heldRenderer,
489 // heldReleaseHooks, heldTimersReplacer) and MovesClaimPolicy decides
490 // them, but they are granted only once this box's moves source is
491 // registered with the claim. Until the wake-source gate opens
492 // (movesWakeSourceRegistered, Q1's one gate) the landing keeps refusing
493 // here, as before; once it opens it lands. RollbackClaimWired stays
494 // (and stays a legal reason in provision), because old records carry it.
495 if hostwrite.Enabled(os.Getenv) {
496 if err := movesWakeSourceRegistered(); err != nil {
497 log.Printf("moves: %s: landing %s under CARLOS_HOST_CLAIM before the wake source is registered: %v", host, id, err)
498 return l.markRollback(ctx, provision.RollbackClaimWired)
499 }
500 }
501 obs, err := l.claim(ctx)
502 if err != nil {
503 return false, err
504 }
505 if op := obs.Claim.Op; op.Kind == hostclaim.KindMoving && op.Phase == hostclaim.PhaseSealed && op.Exec == "" && op.Journal[hostclaim.JournalEvidenceSource] != "" {
506 // The record flipped and the source died before the claim half
507 // (ruling G12): the target writes it, with the same preconditions
508 // the source would have met.
509 h := obs.Claim.Holder
510 if obs, err = hostclaim.Update(ctx, l.s, obs, hostclaim.Flip(id, cfg.Box, h.Stamp, h.Generation)); err != nil {
511 return false, fmt.Errorf("the claim half of the flip: %w", err)
512 }
513 log.Printf("moves: %s: the claim half of the flip was written by the target", host)
514 if err := l.fault("after-target-flip"); err != nil {
515 return false, err
516 }
517 }
518 if err := l.requirePhase(obs, provision.LandChecking); err != nil {
519 return false, err
520 }
521 if cfg.Box == "" || obs.Claim.Op.Exec != cfg.Box {
522 return false, fmt.Errorf("move %s: the claim's executor is %q, not this box", id, obs.Claim.Op.Exec)
523 }
524 if cfg.registerLanding != nil {
525 if err := cfg.registerLanding(ctx, obs); err != nil {
526 return false, err
527 }
528 }
529 rt, err := l.route()
530 if err != nil {
531 return false, err
532 }
533 if rt.DBPath != l.dbPath() {
534 log.Printf("moves: %s: the held row's database is at %s, not the flat %s", host, rt.DBPath, l.dbPath())
535 return l.markRollback(ctx, provision.RollbackLandingLayout)
536 }
537 if !registry.IsHold(rt.Status) || rt.HoldOp != id {
538 return false, fmt.Errorf("the registry row is %q held by %q, not held by landing %s", rt.Status, rt.HoldOp, id)
539 }
540 holder := *obs.Claim.Holder
541 if _, err := writerLease(cfg.OwnPrefix, host, holder); err != nil {
542 return false, fmt.Errorf("move %s: %w", id, err)
543 }
544 src := l.rec.Move.Source
545 if src == nil || src.Evidence == "" {
546 return false, fmt.Errorf("move %s: the record has no source baselines", id)
547 }
548 if obs.Claim.Op.Journal[hostclaim.JournalEvidenceSource] != src.Evidence {
549 return false, fmt.Errorf("move %s: the claim cites %q, the record %q", id, obs.Claim.Op.Journal[hostclaim.JournalEvidenceSource], src.Evidence)
550 }
551 report, err := readStoppedReport(ctx, l.s, src.Evidence)
552 if err != nil {
553 return false, err
554 }
555 if report.Host != host || report.OpID != id || report.Role != "source" || report.FinalTXID != src.FinalTXID {
556 return false, fmt.Errorf("move %s: %s is %s %s %s at TXID %d, not this move's source at %d", id, src.Evidence, report.Host, report.OpID, report.Role, report.FinalTXID, src.FinalTXID)
557 }
558 if !containsString(obs.Claim.Touched.Boxes, cfg.Box) {
559 if obs, err = hostclaim.Update(ctx, l.s, obs, hostclaim.Touch(id, nil, []string{cfg.Box})); err != nil {
560 return false, fmt.Errorf("touch the claim: %w", err)
561 }
562 }
563 own, err := l.w.mirror(ctx, cfg.OwnBucketRegion)
564 if err != nil {
565 return false, err
566 }
567 body, err := own.Get(ctx, cfg.OwnBucket, moves.MoveJSONKey(host, id))
568 if err != nil {
569 return false, fmt.Errorf("read move.json: %w", err)
570 }
571 var mj moves.MoveJSON
572 if err := json.Unmarshal(body, &mj); err != nil {
573 return false, fmt.Errorf("move.json: %w", err)
574 }
575 if mj.Incarnation != l.rec.Move.Incarnation || holder.Incarnation != l.rec.Move.Incarnation {
576 return false, fmt.Errorf("move %s: move.json names incarnation %q, the record %q", id, mj.Incarnation, l.rec.Move.Incarnation)
577 }
578 if err := l.ensureLocalDirs(); err != nil {
579 return false, err
580 }
581 j, err := l.journal()
582 if err != nil {
583 return false, err
584 }
585 captured, err := hostguard.Capture(cfg.DataDir, host)
586 switch {
587 case errors.Is(err, hostguard.ErrNoRecord):
588 // No earlier stay on this box.
589 case err != nil:
590 return false, fmt.Errorf("read the installation record: %w", err)
591 case !j.Done(landStepPrevious) && captured.Stamp != 0 && captured.Stamp != holder.Stamp && hostclaim.InventoryHasStamp(obs.Claim, cfg.Box, captured.Stamp):
592 j.Phase = provision.LandChecking
593 j.Mark(landStepPrevious, strconv.FormatUint(captured.Stamp, 10))
594 if err := j.Commit(cfg.DataDir); err != nil {
595 return false, err
596 }
597 log.Printf("moves: %s: an earlier stay at stamp %d will be retired by local", host, captured.Stamp)
598 }
599 if err := l.fault("after-previous"); err != nil {
600 return false, err
601 }
602 if err := l.guard.Activate(ctx, host, cfg.Box, nil); err != nil {
603 return false, fmt.Errorf("activate the installation record: %w", err)
604 }
605 if err := l.fault("after-activate"); err != nil {
606 return false, err
607 }
608 if j.Done(landStepPrevious) {
609 if obs, err = l.claim(ctx); err != nil {
610 return false, err
611 }
612 if _, err := hostclaim.Update(ctx, l.s, obs, hostclaim.QueueLocalPark(id, cfg.Box, obligationID)); err != nil {
613 return false, fmt.Errorf("queue the local park: %w", err)
614 }
615 }
616 // Spec §7.6 checking: the lease on the location in this box's own
617 // bucket, held through published (M5). The residue parks run under it
618 // (ParkRunHeld), so the landing never waits on its own lock.
619 if _, waiting, err := acquireOwnLease(ctx, l.w, host, holder); waiting || err != nil {
620 return false, err
621 }
622 return l.advance(ctx, provision.LandChecking, provision.LandParking, nil)
623}
624
625func srcTXID(src *provision.MoveSource) uint64 {
626 if src == nil {
627 return 0
628 }
629 return src.FinalTXID
630}
631
632// installSource binds the shared installer (movesinstall.go) to this
633// landing: the claim follows each record step (syncClaimPhase walks it,
634// mirrorPhase writes it), the claim check is the landing op at the phase
635// with this box executing, the record check a landing at the phase with no
636// rollback reason, the refusal the rollback marker (Plan 4's rollback
637// transition replaces markRollback), the phase advance MoveLandingPhase
638// stamping landing.installed_db_sha256 at the end of configured, and the
639// generation, the hold and the archive all the move id's.
640func (l *landingRun) installSource() installSource {
641 m := l.rec.Move
642 return installSource{
643 OpID: l.g.ID, Host: l.rec.Host, Role: roleTarget, Name: "landing",
644 Lane: moves.StagingPrefix(l.rec.Host, l.g.ID), Seal: journalledSeal, Baseline: m.Source,
645 GenerationOp: l.g.ID, HoldOp: l.g.ID, PinInputs: m.Inputs,
646 Phases: [6]string{provision.LandParking, provision.LandBuilding, provision.LandPublished,
647 provision.LandLocal, provision.LandConfigured, provision.LandRestored},
648 Phase: func(from, to string, st installStamp) func(*provision.Record) error {
649 var stamp func(*provision.MoveState)
650 if sha := st.InstalledDBSHA256; sha != "" {
651 stamp = func(m *provision.MoveState) {
652 if m.Landing == nil {
653 m.Landing = &provision.MoveLandingJournal{}
654 }
655 m.Landing.InstalledDBSHA256 = sha
656 }
657 }
658 return provision.MoveLandingPhase(l.g, from, to, stamp)
659 },
660 Mirror: l.mirrorPhase,
661 Sync: l.syncClaimPhase,
662 ClaimCheck: l.landingCheck,
663 RecordPhase: func(rec *provision.Record) (string, error) {
664 m := rec.Move
665 if m == nil || m.ID != l.g.ID || m.Incarnation != l.g.Incarnation || m.State != provision.MoveLanding || m.Reason != "" {
666 return "", fmt.Errorf("%w: the record is no longer landing %s", errLandingChanged, l.g.ID)
667 }
668 return m.LandingPhase, nil
669 },
670 Refuse: func(ctx context.Context, why installRefusal) (bool, string, error) {
671 var reason string
672 switch why {
673 case refuseFilesOccupied:
674 reason = provision.RollbackFilesOccupied
675 case refuseManifestOccupied:
676 reason = provision.RollbackManifestOccupied
677 case refuseInstallMismatch:
678 reason = provision.RollbackInstallMismatch
679 case refuseConfigChanged:
680 reason = provision.RollbackConfigChanged
681 case refuseDeadline:
682 reason = provision.RollbackLandingDeadline
683 default:
684 return false, "", fmt.Errorf("move %s: a landing has no refusal %d", l.g.ID, why)
685 }
686 ok, err := l.markRollback(ctx, reason)
687 if !ok {
688 return false, "", err
689 }
690 return true, reason, err
691 },
692 PastDeadline: l.pastDeadline,
693 }
694}
695