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 "sort"
12 "strconv"
13 "strings"
14 "time"
15
16 "github.com/carlosframework/platform/internal/hostclaim"
17 "github.com/carlosframework/platform/internal/hostfiles"
18 "github.com/carlosframework/platform/internal/hostguard"
19 "github.com/carlosframework/platform/internal/hostwrite"
20 "github.com/carlosframework/platform/internal/lease"
21 "github.com/carlosframework/platform/internal/moves"
22 "github.com/carlosframework/platform/internal/moves/archive"
23 "github.com/carlosframework/platform/internal/park"
24 "github.com/carlosframework/platform/internal/provision"
25 "github.com/carlosframework/platform/internal/registry"
26 "github.com/carlosframework/platform/internal/release"
27)
28
29// The generation installer (moves spec §7.6, §7.8; Plan 4 Task 3), shared by
30// the landing on the target and the dirty return on the source. It runs the
31// six phases parking, building, published, local, configured and restored
32// exactly as Plan 2 Task 10 wrote them for the landing; installSource says
33// what differs between the two kinds of install (rulings J13, J14, J20, J24):
34//
35// - the claim check: a landing's op is landing at the phase, mirrored into
36// the claim by every record step; a return's op is restoring/claimed with
37// the source executing at its pre-move stamp S throughout, and the claim
38// does not follow the source's install phases;
39// - the record check: landing at the phase with no rollback reason, or
40// rolling_back, dirty, with the source at the phase;
41// - the refusal action: a landing marks its rollback (MoveMarkRollback); a
42// return stops for an operator (MoveMarkStuck) with a recorded reason;
43// - the phase advance: MoveLandingPhase or MoveRollbackSourcePhase, bound
44// by the caller, so neither kind is told apart by a type switch;
45// - the generation: the move id for a landing, the rollback id for a return
46// (J14: a return is cold and never reuses S's generation name). It names
47// the build, the install's own journal, <dbPath>.gen and .files-gen;
48// - configured: a landing renders and requires its pinned inputs; a return
49// renders the current ones and records them in Rollback.RecoveryInputs,
50// never compared with move.inputs (J2-R2-5);
51// - local: a landing parks an earlier stay's artifacts tagged S'; a return
52// parks the source's own pre-move installation before anything is
53// extracted (J13, fix round 1): RetireArtifacts for S and for every
54// stamp the claim shows superseded on this box, a stuck mark for any
55// other stamp, then the moves-owned rename (H5(b), parkLocalArtifacts)
56// of every pre-move artifact still at the flat paths, inside the host
57// guard. placeStaged and restore then find nothing there, and refuse
58// anything that appears;
59// - the authoritative region the claim's view names for its parks (J24,
60// §5.1): the source's for a landing, the target's for a return, each the
61// other side of the move before this side's probe is admitted.
62//
63// The phase-name convention is Plan 2's (G8): restored is written by the CAS
64// that ends configured, after the database is installed and its markers
65// written. Every record write goes through a provision transition (the
66// record_seq rule). The installer is unarmed in production, as the landing
67// is (until the wake-source gate opens, movesWakeSourceRegistered, Plan 5
68// Task 9b): only the landing's pass,
69// the recovery and the tests call it.
70
71// installStepPreMove is the moves-owned rename of a return's pre-move
72// artifacts (parkLocalArtifacts' step: the retirement id).
73const installStepPreMove = "pre_move_parked"
74
75// installStepResidueKept is a dirty return's no-op parking (Task 9a fix
76// round 3, ruling (a)): the sentence saying the target never admitted the
77// move, so the pre-move generation stays the source's live state.
78const installStepResidueKept = "residue_kept"
79
80// installStamp is what a phase step records besides the phase: at the CAS
81// that ends configured, the installed database's digest (a landing records
82// it in landing.installed_db_sha256) and the inputs a return rendered.
83type installStamp struct {
84 InstalledDBSHA256 string
85 RecoveryInputs *provision.MoveInputs
86}
87
88// installRefusal is why an install stops for good: installSource.Refuse maps
89// it to its kind's recorded sentence.
90type installRefusal int
91
92const (
93 refuseFilesOccupied installRefusal = iota + 1 // local: the files paths hold what the install did not write
94 refuseInstallMismatch // configured: the build does not verify
95 refuseConfigChanged // configured: a landing's render is not its pin
96 refuseDeadline // configured: a landing ran past its deadline
97 refuseUnknownStamp // local: a return found paths under a stamp it cannot account for
98 refuseManifestOccupied // local: the prepared files manifest's key holds a manifest the install did not write
99)
100
101// installSource is one kind of install: what the six phases need from the
102// caller (Plan 4 Task 3's interface, with the rulings' additions).
103type installSource struct {
104 OpID, Host string
105 // Role is roleTarget for a landing, roleSource for a dirty return.
106 Role string
107 // Name is the install's word in the log: "landing" or "return".
108 Name string
109 // Lane is the staging or return lane prefix in this box's own bucket;
110 // every object the seal names must be under it.
111 Lane string
112 // Seal reads the sealed set from the claim the phase works from.
113 Seal func(*hostclaim.Claim) (moves.Seal, error)
114 // Baseline is what the sealed set restores to: the final TXID, the
115 // files and timers digests, and the carried columns.
116 Baseline *provision.MoveSource
117 // GenerationOp names the generation this install builds and the
118 // install's own journal; HoldOp is the op the row is held by.
119 GenerationOp, HoldOp string
120 // PinInputs are a landing's pinned inputs; nil for a return.
121 PinInputs *provision.MoveInputs
122 // Phases are the six phase names of this install, in order.
123 Phases [6]string
124 // Phase is the record transition one step makes, bound to the guard.
125 Phase func(from, to string, st installStamp) func(*provision.Record) error
126 // Mirror is the claim half of a record step; nil when the claim does
127 // not follow this install's phases.
128 Mirror func(ctx context.Context, from, to string) error
129 // Sync is each phase's first claim read (a landing walks a claim that
130 // is behind forward); ClaimCheck is what the phase and the guard require
131 // of the claim at phase.
132 Sync func(ctx context.Context) (hostclaim.Observed, error)
133 ClaimCheck func(phase string) func(*hostclaim.Claim) error
134 // RecordPhase answers the phase a record is at while it is still this
135 // install's, and an error otherwise.
136 RecordPhase func(*provision.Record) (string, error)
137 // Refuse writes the refusal: written is true when the record took it,
138 // sentence is the recorded reason.
139 Refuse func(ctx context.Context, why installRefusal) (written bool, sentence string, err error)
140 // PastDeadline reports a landing's deadline passed; nil for a return.
141 PastDeadline func() bool
142}
143
144func (src installSource) validate() error {
145 if src.OpID == "" || src.Host == "" || (src.Role != roleTarget && src.Role != roleSource) || src.Lane == "" || src.Seal == nil ||
146 src.GenerationOp == "" || src.HoldOp == "" || src.Phase == nil || src.Sync == nil || src.ClaimCheck == nil ||
147 src.RecordPhase == nil || src.Refuse == nil {
148 return errors.New("moves: an install source names its op, role, lane, seal, generation and hooks")
149 }
150 for _, p := range src.Phases {
151 if p == "" {
152 return errors.New("moves: an install source names its six phases")
153 }
154 }
155 return nil
156}
157
158// kind is the claim op kind this install runs under.
159func (src installSource) kind() string {
160 if src.Role == roleSource {
161 return hostclaim.KindRestoring
162 }
163 return hostclaim.KindLanding
164}
165
166// installRun is one call's state for one install.
167type installRun struct {
168 w *movesWorker
169 s release.Store
170 rec provision.Record
171 g provision.MoveGuard
172 src installSource
173 guard hostguard.Guard
174 local park.Local
175 // refused is the sentence a refusal recorded during this call.
176 refused string
177}
178
179// installGeneration runs the record's current install phase once (one phase
180// per call, as the landing's pass and the cut step): parking, building,
181// published, local or configured. advanced is true when it wrote the record
182// (a phase step or the refusal); refused is the recorded sentence when the
183// refusal was written. A record at restored is the probe's and is left as it
184// is; one that is no longer this install's is an error.
185func installGeneration(ctx context.Context, s release.Store, w *movesWorker, rec provision.Record, src installSource) (advanced bool, refused string, err error) {
186 if err := src.validate(); err != nil {
187 return false, "", err
188 }
189 phase, err := src.RecordPhase(&rec)
190 if err != nil {
191 return false, "", err
192 }
193 if rec.Move.ID != src.OpID || rec.Host != src.Host {
194 return false, "", fmt.Errorf("moves: the install of %s is not %s's", src.OpID, rec.Host)
195 }
196 ir := newInstallRun(s, w, rec, src)
197 switch phase {
198 case src.Phases[0]:
199 advanced, err = ir.parking(ctx)
200 case src.Phases[1]:
201 advanced, err = ir.building(ctx)
202 case src.Phases[2]:
203 advanced, err = ir.published(ctx)
204 case src.Phases[3]:
205 advanced, err = ir.local_(ctx)
206 case src.Phases[4]:
207 advanced, err = ir.configured(ctx)
208 case src.Phases[5]:
209 return false, "", nil
210 default:
211 return false, "", fmt.Errorf("moves: %s is not a phase of the %s's install", phase, src.Name)
212 }
213 return advanced, ir.refused, err
214}
215
216func newInstallRun(s release.Store, w *movesWorker, rec provision.Record, src installSource) *installRun {
217 cfg := w.cfg
218 ir := &installRun{w: w, s: s, rec: rec, g: provision.MoveGuard{ID: rec.Move.ID, Incarnation: rec.Move.Incarnation}, src: src}
219 ir.guard = cfg.Guard
220 if ir.guard.Claims == nil {
221 ir.guard.Claims = hostguard.StoreReader{Store: s}
222 }
223 ir.local = cfg.Local
224 ir.local.Guard, ir.local.Claims = ir.guard, s
225 if ir.local.Box == "" {
226 ir.local.Box = cfg.Box
227 }
228 if ir.local.Authorize == nil {
229 // park.Local's authorizer: the claim's MoveParks local authorizer
230 // (ParkLocalAuthorize) behind this install's own check; the claim's
231 // engine authorizer refuses a park-local (M9). Without it every local
232 // chore refuses (landingArmed requires it).
233 ir.local.Authorize = movesLocalAuthorize(cfg.ParkLocalAuthorize, ir.authorizeChore)
234 }
235 return ir
236}
237
238func (ir *installRun) cfg() moveCfg { return ir.w.cfg }
239
240// installAuthority is the install's authority re-check for the prepared
241// manifest's bucket writes (fix round 1, finding 1; region HA's
242// authorityStore pattern): a fresh read of the claim, the install's own
243// claim check at phase (the op at that phase with this box executing) and
244// the holder still at the captured stamp. It runs immediately before every
245// write attempt, retries included, so a delayed build whose executor,
246// phase or stamp moved writes nothing more.
247func (ir *installRun) installAuthority(ctx context.Context, captured hostguard.Captured, phase string) func() error {
248 check := ir.src.ClaimCheck(phase)
249 return func() error {
250 if err := ctx.Err(); err != nil {
251 return err
252 }
253 obs, err := hostclaim.Read(ctx, ir.s, ir.rec.Host)
254 if err != nil {
255 return err
256 }
257 if obs.Absent || obs.Claim == nil {
258 return fmt.Errorf("%w: %s has no claim", errLandingChanged, ir.rec.Host)
259 }
260 if err := check(obs.Claim); err != nil {
261 return err
262 }
263 if obs.Claim.Holder == nil || obs.Claim.Holder.Stamp != captured.Stamp {
264 return fmt.Errorf("%w: the claim's holder is not at the installation stamp %d", errLandingChanged, captured.Stamp)
265 }
266 return nil
267 }
268}
269
270// installAuthorityStore is the versioned store the prepared build writes
271// through: every write re-checks the install's authority first. Reads are
272// the store's own; the build never deletes.
273type installAuthorityStore struct {
274 lease.VersionedStore
275 check func() error
276}
277
278func (s installAuthorityStore) PutIfAbsent(ctx context.Context, bucket, key string, body []byte) (string, error) {
279 if err := s.check(); err != nil {
280 return "", err
281 }
282 return s.VersionedStore.PutIfAbsent(ctx, bucket, key, body)
283}
284
285func (s installAuthorityStore) CopyVersion(ctx context.Context, sb, sk, sv, db, dk string, createOnly bool) (string, string, error) {
286 if err := s.check(); err != nil {
287 return "", "", err
288 }
289 return s.VersionedStore.CopyVersion(ctx, sb, sk, sv, db, dk, createOnly)
290}
291
292func (installAuthorityStore) DeleteVersion(context.Context, string, string, string) error {
293 return fmt.Errorf("%w: the prepared build never deletes", errLandingChanged)
294}
295
296// preparedManifestJournalled reports whether the journal records the
297// prepared files manifest as built: the step done with this install's
298// binding (decodePreparedManifest: P's key, the op and the captured
299// stamp), not the "skipped" value journals written before C8 existed,
300// which a replay builds (ruling Q3 item 2; the step itself is create-only
301// and replay-safe). Any other value is an error (fix round 1, finding 3):
302// an empty or malformed step, or a binding for another key, op or stamp,
303// is a damaged or foreign journal, and the install neither advances over
304// it nor rebuilds over it.
305func preparedManifestJournalled(j *moves.Journal, key, op string, stamp uint64) (bool, error) {
306 v, ok := j.Steps[landStepFilesManifest]
307 if !ok || v == landingFilesManifestSkipped {
308 return false, nil
309 }
310 if _, err := decodePreparedManifest(v, key, op, stamp); err != nil {
311 return false, err
312 }
313 return true, nil
314}
315
316// fault is the test seam after a side effect (the landing's points).
317func (ir *installRun) fault(point string) error {
318 if f := ir.w.cfg.landFault; f != nil {
319 return f(point)
320 }
321 return nil
322}
323
324// sync is each phase's first step: the kind's claim read, then its check at
325// phase (K5's revalidation).
326func (ir *installRun) sync(ctx context.Context, phase string) (hostclaim.Observed, error) {
327 obs, err := ir.src.Sync(ctx)
328 if err != nil {
329 return obs, err
330 }
331 if err := ir.src.ClaimCheck(phase)(obs.Claim); err != nil {
332 return obs, fmt.Errorf("move %s: %w", ir.g.ID, err)
333 }
334 return obs, nil
335}
336
337// seal reads the sealed set from the claim and requires every object it
338// names in this install's lane.
339func (ir *installRun) seal(c *hostclaim.Claim) (moves.Seal, error) {
340 seal, err := ir.src.Seal(c)
341 if err != nil {
342 return seal, err
343 }
344 for _, class := range [][]moves.SealedObject{seal.Replica, seal.Files, seal.Timers, seal.State} {
345 for _, o := range class {
346 if !strings.HasPrefix(o.Key, ir.src.Lane) {
347 return seal, fmt.Errorf("%w: %s is not in the %s's lane %s", moves.ErrBadSeal, o.Key, ir.src.Name, ir.src.Lane)
348 }
349 }
350 }
351 return seal, nil
352}
353
354// transition writes one record transition. A conflict is someone else's
355// decision (a rollback, a stuck mark, a console write): logged, and the call
356// ends without error.
357func (ir *installRun) transition(ctx context.Context, what string, mut func(*provision.Record) error) (bool, error) {
358 var pre provision.Record
359 after, err := provision.SetRecordMove(ctx, ir.s, ir.rec.Account, ir.rec.App, ir.rec.Host, observedMove(&pre, mut))
360 if err != nil {
361 if errors.Is(err, provision.ErrMoveConflict) {
362 log.Printf("moves: %s: %s of %s refused (%v); the record moved on", ir.rec.Host, what, ir.g.ID, err)
363 return false, nil
364 }
365 return false, fmt.Errorf("%s: %w", what, err)
366 }
367 ir.w.logTransition(ctx, ir.s, pre, *after)
368 return true, nil
369}
370
371// advance writes the record's step and then, for a landing, mirrors it into
372// the claim. A mirror that fails is logged: the next pass's sync writes it.
373func (ir *installRun) advance(ctx context.Context, from, to string, st installStamp) (bool, error) {
374 name := ir.src.Name
375 ok, err := ir.transition(ctx, "the "+name+" "+from+" to "+to, ir.src.Phase(from, to, st))
376 if !ok {
377 return ok, err
378 }
379 log.Printf("moves: %s: %s %s is %s", ir.rec.Host, name, ir.g.ID, to)
380 if err := ir.fault("after-record-" + to); err != nil {
381 return true, err
382 }
383 if ir.src.Mirror != nil {
384 if err := ir.src.Mirror(ctx, from, to); err != nil {
385 log.Printf("moves: %s: the claim half of %s %s waits for the next pass: %v", ir.rec.Host, name, to, err)
386 }
387 }
388 return true, nil
389}
390
391// refuse writes the kind's refusal; written is true when the record took it.
392func (ir *installRun) refuse(ctx context.Context, why installRefusal) (bool, error) {
393 ok, sentence, err := ir.src.Refuse(ctx, why)
394 if ok {
395 ir.refused = sentence
396 }
397 return ok, err
398}
399
400func (ir *installRun) pastDeadline() bool { return ir.src.PastDeadline != nil && ir.src.PastDeadline() }
401
402// route is the host's registry row on this box.
403func (ir *installRun) route() (registry.Route, error) {
404 rt, err := ir.cfg().Route(ir.rec.Host)
405 if err != nil {
406 return registry.Route{}, fmt.Errorf("read the registry row: %w", err)
407 }
408 if rt == nil {
409 return registry.Route{}, fmt.Errorf("%s has no registry row on this box", ir.rec.Host)
410 }
411 return *rt, nil
412}
413
414func (ir *installRun) dbPath() string { return provision.DefaultDBPath(ir.cfg().DataDir, ir.rec.Host) }
415
416// journal is the install's own op journal, named by its generation.
417func (ir *installRun) journal() (*moves.Journal, error) {
418 return moves.ReadJournal(ir.cfg().DataDir, ir.rec.Host, ir.src.GenerationOp)
419}
420
421func (ir *installRun) journalDir() string {
422 return moves.JournalDir(ir.cfg().DataDir, ir.rec.Host, ir.src.GenerationOp)
423}
424
425// buildDir is BuildGeneration's scratch, <JournalDir>/build/ (ruling G34),
426// on the data disk so the result renames into DBPath.
427func (ir *installRun) buildDir() string { return filepath.Join(ir.journalDir(), "build") }
428
429// versioned is this box's own bucket by exact version.
430func (ir *installRun) versioned(ctx context.Context) (lease.VersionedStore, error) {
431 cfg := ir.cfg()
432 if cfg.Versioned == nil {
433 return nil, errors.New("no versioned store on this box")
434 }
435 return cfg.Versioned(ctx, cfg.OwnBucket, cfg.OwnBucketRegion)
436}
437
438// parking (§7.6, D6): whatever an earlier stay of this incarnation left at
439// holder.location in this box's own bucket is parked before the build
440// writes beside it, one park-generation chore per generation (the origin as
441// @root), scoped to the location with the flip's time as the evidence
442// horizon. On the target that is an earlier stay's residue; on the source
443// of a dirty return it is the pre-move generations (§7.8, D6). Each chore is
444// run once per pass through the park engine. The location's lease is the
445// install's own, held from here (a landing's from checking) through
446// published, and each chore runs under it through park.Engine.RunHeld over
447// the claim's leases (ParkRunHeld, M5). ErrPending is the expected answer: the acknowledgement waits for
448// the horizon, and is C4's runner's. The phase advances once every chore's
449// manifest exists and its deletion set is gone (Engine.Parked).
450// Per-generation chores are what let the acknowledgement finish after the
451// probe makes the new generation live here.
452//
453// A replay finds parking and continues from the claim: the residue list is
454// journalled so the chores are the same ones, the queue is idempotent per
455// generation, and Engine.Run resumes a chore from its manifest.
456func (ir *installRun) parking(ctx context.Context) (bool, error) {
457 cfg := ir.cfg()
458 host, id, gen := ir.rec.Host, ir.g.ID, ir.src.GenerationOp
459 phase, next := ir.src.Phases[0], ir.src.Phases[1]
460 obs, err := ir.sync(ctx, phase)
461 if err != nil {
462 return false, err
463 }
464 holder := *obs.Claim.Holder
465 location := strings.TrimSuffix(holder.Location, "/")
466 vs, err := ir.versioned(ctx)
467 if err != nil {
468 return false, err
469 }
470 j, err := ir.journal()
471 if err != nil {
472 return false, err
473 }
474 _, listed := j.Steps[landStepResidue]
475 if !listed && ir.src.Role == roleSource && !targetAdmitted(ir.rec.Move, obs.Claim) {
476 // A dirty return whose target never admitted this move (ruling
477 // (a), fix round 3): holder.generation is still the pre-move one,
478 // the source's live state, so there is no residue to park. This
479 // is decided from the claim's recorded admissions only (fix round
480 // 4: not the landing phase), never from a park view's
481 // refusal, which can be transient.
482 why := fmt.Sprintf("the target never admitted move %s: the claim holds no admission of its generation; the pre-move generation stays the source's live state", id)
483 j.Phase = phase
484 j.Mark(landStepResidue, "[]")
485 j.Mark(installStepResidueKept, why)
486 if err := j.Commit(cfg.DataDir); err != nil {
487 return false, err
488 }
489 log.Printf("moves: %s: the %s parks nothing: %s", host, ir.src.Name, why)
490 return ir.advance(ctx, phase, next, installStamp{})
491 }
492 var gens []string
493 if raw, ok := j.Steps[landStepResidue]; ok {
494 if err := json.Unmarshal([]byte(raw), &gens); err != nil {
495 return false, fmt.Errorf("the journal's residue list: %w", err)
496 }
497 } else {
498 objs, err := vs.ListVersions(ctx, cfg.OwnBucket, location+"/")
499 if err != nil {
500 return false, fmt.Errorf("list %s: %w", location, err)
501 }
502 seen := map[string]bool{}
503 for _, o := range objs {
504 if o.Key == location+"/lock.json" {
505 continue
506 }
507 if movesHolderBuildState(location, o.Key) {
508 // The build state beside the lease is the marker's twin
509 // (ruling R-PARK): no generation's, never residue here.
510 // The build places the moved state over it.
511 continue
512 }
513 g, ok := landingGenerationOf(location, o.Key)
514 if !ok {
515 return false, fmt.Errorf("%s is not an object of a generation this %s can classify", o.Key, ir.src.Name)
516 }
517 if g == gen || g == landingMarkerGeneration {
518 // The install's own generation is never residue, and the
519 // marker is superseded by the probe, never parked.
520 continue
521 }
522 if g == "" {
523 g = hostclaim.RootGeneration
524 }
525 seen[g] = true
526 }
527 for g := range seen {
528 gens = append(gens, g)
529 }
530 sort.Strings(gens)
531 body, err := json.Marshal(gens)
532 if err != nil {
533 return false, err
534 }
535 j.Phase = phase
536 j.Mark(landStepResidue, string(body))
537 if err := j.Commit(cfg.DataDir); err != nil {
538 return false, err
539 }
540 }
541 if len(gens) == 0 {
542 return ir.advance(ctx, phase, next, installStamp{})
543 }
544 if ir.rec.Move.FlippedAt == nil {
545 return false, fmt.Errorf("move %s: the record has no flip time", id)
546 }
547 // The parks run under the build's own lease on the location, which the
548 // install holds from here (a landing from checking) through published:
549 // the residue scope's lease set is that lock (M6), so Engine.Run would
550 // wait on the install itself. park.Engine.RunHeld (ParkRunHeld) runs
551 // each chore under the held lease instead, borrowed through the claim's
552 // AggregateLeases (M5).
553 held, waiting, err := ir.lease(ctx, holder)
554 if waiting || err != nil {
555 return false, err
556 }
557 scope := moves.ResidueScope(holder, *ir.rec.Move.FlippedAt)
558 if obs, err = hostclaim.Update(ctx, ir.s, obs, hostclaim.QueueResidueParks(id, cfg.parkRegion(), scope, gens, obligationID)); err != nil {
559 return false, fmt.Errorf("queue the residue parks: %w", err)
560 }
561 if err := ir.fault("after-residue-queued"); err != nil {
562 return false, err
563 }
564 eng := ir.engine(vs)
565 pending := 0
566 for _, o := range obs.Claim.Obligations {
567 if o.Op != id || o.Kind != "park-generation" || !park.PlacementMatches(cfg.ParkHomeRegion, cfg.Region, o.Region) {
568 continue
569 }
570 w := park.Work{Host: host, ID: o.ID, Op: id}
571 if cfg.ParkRunHeld == nil {
572 return false, fmt.Errorf("move %s: %w", id, &ClaimDependencyError{Contracts: []string{"the claim's MoveParks RunHeld (armMovesParks)"}})
573 }
574 if err := cfg.ParkRunHeld(ctx, eng, w, held); err != nil && !errors.Is(err, park.ErrPending) {
575 return false, fmt.Errorf("park generation %s: %w", o.Generation, err)
576 }
577 parked, err := eng.Parked(ctx, w)
578 if errors.Is(err, park.ErrChanged) {
579 continue // acknowledged already
580 }
581 if err != nil {
582 return false, fmt.Errorf("park generation %s: %w", o.Generation, err)
583 }
584 if !parked {
585 pending++
586 }
587 }
588 if pending > 0 {
589 ir.w.notice(host+"#parking", "moves: %s: %d residue park(s) are not complete yet", host, pending)
590 return false, nil
591 }
592 return ir.advance(ctx, phase, next, installStamp{})
593}
594
595// targetAdmitted reports whether the claim carries evidence that the
596// move's target admitted a writer: the holder's admissions include the
597// move's prepared generation (an admission appends its record atomically,
598// and ReturnToSource keeps them). The record's landing phase is not
599// evidence: it enters probing before the probe runs (fix round 4), so a
600// landing that stopped at probing before its admission never admitted.
601func targetAdmitted(m *provision.MoveState, c *hostclaim.Claim) bool {
602 if m == nil || c == nil || c.Holder == nil {
603 return false
604 }
605 for _, a := range c.Holder.Admissions {
606 if a.Generation == m.ID {
607 return true
608 }
609 }
610 return false
611}
612
613// building (§7.6, §5.1 step 2): the generation built from the seal's pinned
614// replica versions under the location's lease (moves.BuildGeneration: the
615// intent, the snapshot, the anchor, the completed install manifest, every
616// one create-only), with the result left at <JournalDir>/build/<host>.db
617// for the restore. The manifest goes into the journal.
618//
619// A replay finds building and builds again: BuildGeneration reuses what it
620// finds under the generation's keys and refuses anything it did not make.
621func (ir *installRun) building(ctx context.Context) (bool, error) {
622 cfg := ir.cfg()
623 host, id, gen := ir.rec.Host, ir.g.ID, ir.src.GenerationOp
624 phase, next := ir.src.Phases[1], ir.src.Phases[2]
625 obs, err := ir.sync(ctx, phase)
626 if err != nil {
627 return false, err
628 }
629 holder := *obs.Claim.Holder
630 held, waiting, err := ir.lease(ctx, holder)
631 if waiting || err != nil {
632 return false, err
633 }
634 seal, err := ir.seal(obs.Claim)
635 if err != nil {
636 return false, err
637 }
638 src := ir.src.Baseline
639 if src == nil || seal.N != src.FinalTXID {
640 return false, fmt.Errorf("move %s: the seal is at TXID %d and the record at %d", id, seal.N, srcTXID(src))
641 }
642 vs, err := ir.versioned(ctx)
643 if err != nil {
644 return false, err
645 }
646 if err := held.Check(ctx); err != nil {
647 return false, fmt.Errorf("the location's lease: %w", err)
648 }
649 // Every generation request runs under live lease authority (fix round
650 // 1, item 3): the store checks the lease before each call, as the cut's
651 // guarded writes check theirs, the requests run under a context the
652 // lease's loss cancels (leaseContext, fix round 2), and the lease is
653 // checked again before the build's completion is recorded.
654 m, err := moves.BuildGeneration(leaseContext(ctx, held), moves.BuildInputs{Store: leasedStore{VersionedStore: vs, check: held.Check}, Bucket: cfg.OwnBucket, Source: seal.Replica, N: seal.N,
655 GenerationPrefix: moves.GenerationPrefix(holder.Location, gen), Op: gen, Host: host, WorkDir: ir.buildDir(), Now: cfg.Now})
656 if err != nil {
657 return false, fmt.Errorf("build the generation: %w", err)
658 }
659 if err := ir.placeBuildState(leaseContext(ctx, held), leasedStore{VersionedStore: vs, check: held.Check}, seal, holder); err != nil {
660 return false, err
661 }
662 if err := held.Check(ctx); err != nil {
663 return false, fmt.Errorf("the location's lease after the build: %w", err)
664 }
665 body, err := json.Marshal(m)
666 if err != nil {
667 return false, err
668 }
669 j, err := ir.journal()
670 if err != nil {
671 return false, err
672 }
673 j.Phase = phase
674 j.Mark(landStepInstall, string(body))
675 if err := j.Commit(cfg.DataDir); err != nil {
676 return false, err
677 }
678 if err := ir.fault("after-build"); err != nil {
679 return false, err
680 }
681 log.Printf("moves: %s: generation %s built at TXID %d", host, gen, m.N)
682 return ir.advance(ctx, phase, next, installStamp{})
683}
684
685// placeBuildState copies the sealed build state (the seal's State class,
686// the lane's state/ key) beside the location's lease,
687// holder.Location/build-state.json in this box's own bucket (settings spec
688// rev 11 §4.3a, ruling R-LANDING). building calls it under the location's
689// lease, so it lands before the probe and the first wake: their record
690// merges onto the moved state, and a held instance never reads as new
691// here. It copies exactly the sealed version (ruling R-SEAL-STATE): a
692// request a stale round signed can land a newer version in the lane until
693// the seal's bound, and that version is never placed. A seal with no state
694// (the source had none, or a seal from before the class existed) places
695// nothing, and removes every version of a build state an earlier stay left
696// at the location (Task 12a): it is the marker's twin, so the landing's park
697// passes it over, and otherwise that stay's state would read as this
698// instance's. A replay copies, or removes, again.
699func (ir *installRun) placeBuildState(ctx context.Context, vs lease.VersionedStore, seal moves.Seal, holder hostclaim.Holder) error {
700 bucket := ir.cfg().OwnBucket
701 dst := release.BuildStateKey(strings.TrimSuffix(holder.Location, "/"))
702 if len(seal.State) == 0 {
703 vers, err := vs.ListVersions(ctx, bucket, dst)
704 if err != nil {
705 return fmt.Errorf("list a leftover build state at %s: %w", dst, err)
706 }
707 for _, v := range vers {
708 if v.Key != dst {
709 continue
710 }
711 if err := vs.DeleteVersion(ctx, bucket, dst, v.VersionID); err != nil {
712 return fmt.Errorf("remove a leftover build state at %s (version %s): %w", dst, v.VersionID, err)
713 }
714 log.Printf("moves: %s: the %s carries no build state; removed version %s of an earlier stay's at %s", ir.rec.Host, ir.src.Name, v.VersionID, dst)
715 }
716 return nil
717 }
718 o := seal.State[0]
719 if o.Key != ir.src.Lane+moves.BuildStateSuffix {
720 return fmt.Errorf("%w: the sealed build state %s is not the %s's lane key", moves.ErrBadSeal, o.Key, ir.src.Name)
721 }
722 if _, _, err := vs.CopyVersion(ctx, bucket, o.Key, o.VersionID, bucket, dst, false); err != nil {
723 return fmt.Errorf("place the build state at %s: %w", dst, err)
724 }
725 log.Printf("moves: %s: the %s placed the moved build state at %s", ir.rec.Host, ir.src.Name, dst)
726 return nil
727}
728
729// published (§7.6): the build is complete and recorded; generation.json is
730// not written here (R10-3: the probe's cold admission publishes it). The
731// journal records the phase, the record advances to local and the location's
732// lease is released: the build was the only writer that needed it.
733func (ir *installRun) published(ctx context.Context) (bool, error) {
734 phase, next := ir.src.Phases[2], ir.src.Phases[3]
735 if _, err := ir.sync(ctx, phase); err != nil {
736 return false, err
737 }
738 j, err := ir.journal()
739 if err != nil {
740 return false, err
741 }
742 if !j.Done(landStepInstall) {
743 return false, fmt.Errorf("move %s: published without an install manifest in the journal", ir.g.ID)
744 }
745 j.Phase = phase
746 j.Mark(landStepPublished, ir.cfg().Now().UTC().Format(time.RFC3339Nano))
747 if err := j.Commit(ir.cfg().DataDir); err != nil {
748 return false, err
749 }
750 ok, err := ir.advance(ctx, phase, next, installStamp{})
751 if ok {
752 ir.releaseLease()
753 }
754 return ok, err
755}
756
757// local (§7.6), each sub-step journalled and skipped on a replay once done,
758// in this order (round-2 finding 8): (1) the installation record must be at
759// the holder's stamp; (2) what is there before is parked: on the target an
760// earlier stay's artifacts tagged S' through park.Local (which refuses a
761// path tagged with another stamp), or a park-local left with nothing to
762// retire is completed; on the source of a dirty return its own pre-move
763// installation (retirePreMove); (3) the sealed archive, read by exact
764// version and verified, is extracted into this install's own staging,
765// <JournalDir>/extract/, and then, inside the host guard, moved to the files
766// directory and <host>.env when nothing this install did not write is there
767// (installFiles: the install never deletes at the live paths, final review
768// finding 1; a retry clears only its staging); (4) the new paths are
769// registered under the holder's stamp; (5) the timers table is the sealed
770// snapshot with its registrations, the carried columns are set and the
771// restore-verification entry removed; (6) the prepared files manifest (C8,
772// Task 6): the placed tree, <host>.env and the wake state as create-only
773// objects and one create-only sequence-0 manifest under files/g-<gen>/ in
774// the location, each write re-checking the install's authority, the
775// binding journalled with the manifest's digest and exact version, and a
776// manifest already there that is not this install's refused
777// (RollbackManifestOccupied); (7) the files directory is synced
778// and .files-gen written last, so its presence vouches for every step
779// before it.
780//
781// A replay finds local and finishes what the journal does not record;
782// nothing here is read by the probe before .files-gen exists.
783func (ir *installRun) local_(ctx context.Context) (bool, error) {
784 cfg := ir.cfg()
785 host, id, gen := ir.rec.Host, ir.g.ID, ir.src.GenerationOp
786 phase, next := ir.src.Phases[3], ir.src.Phases[4]
787 obs, err := ir.sync(ctx, phase)
788 if err != nil {
789 return false, err
790 }
791 holder := *obs.Claim.Holder
792 src := ir.src.Baseline
793 if src == nil || src.Files == nil {
794 return false, fmt.Errorf("move %s: the record has no source baselines", id)
795 }
796 seal, err := ir.seal(obs.Claim)
797 if err != nil {
798 return false, err
799 }
800 j, err := ir.journal()
801 if err != nil {
802 return false, err
803 }
804 j.Phase = phase
805 mark := func(step, value string) error {
806 j.Mark(step, value)
807 if err := j.Commit(cfg.DataDir); err != nil {
808 return err
809 }
810 return ir.fault("after-" + step)
811 }
812 captured, err := ir.capture(holder)
813 if err != nil {
814 return false, err
815 }
816 if !j.Done(landStepRetired) {
817 if ir.src.Role == roleSource {
818 outcome, err := ir.retirePreMove(ctx, captured, j, mark)
819 switch {
820 case err != nil:
821 return false, err
822 case outcome == restoreMarked:
823 return true, nil // the record changed: the refusal
824 case outcome == restoreWaiting:
825 return false, nil
826 }
827 } else if raw, ok := j.Steps[landStepPrevious]; ok {
828 earlier, err := strconv.ParseUint(raw, 10, 64)
829 if err != nil || earlier == 0 {
830 return false, fmt.Errorf("the journal's previous stamp %q", raw)
831 }
832 var chore *hostclaim.Obligation
833 for i := range obs.Claim.Obligations {
834 if o := &obs.Claim.Obligations[i]; o.Op == id && o.Kind == "park-local" && o.Box == cfg.Box {
835 chore = o
836 }
837 }
838 switch {
839 case chore != nil && hostclaim.InventoryHasStamp(obs.Claim, cfg.Box, earlier):
840 if err := ir.local.RetireArtifacts(ctx, host, captured, chore.ID, id, earlier); err != nil {
841 return false, fmt.Errorf("retire the earlier stay's artifacts: %w", err)
842 }
843 log.Printf("moves: %s: the earlier stay's artifacts (stamp %d) are parked", host, earlier)
844 case chore != nil:
845 if _, err := hostclaim.Update(ctx, ir.s, obs, hostclaim.CompleteLocalPark(id, cfg.Box, earlier)); err != nil {
846 return false, fmt.Errorf("complete the local park: %w", err)
847 }
848 }
849 }
850 if err := mark(landStepRetired, ""); err != nil {
851 return false, err
852 }
853 }
854 dir, envPath := cfg.FilesDir(host)
855 if !j.Done(landStepExtracted) {
856 outcome, err := ir.installFiles(ctx, j, mark, captured, seal, *src.Files, dir, envPath)
857 switch {
858 case err != nil:
859 return false, err
860 case outcome == restoreMarked:
861 return true, nil // the record changed: the refusal
862 case outcome == restoreWaiting:
863 return false, nil
864 }
865 }
866 if !j.Done(landStepInventory) {
867 // Only what this install has placed (carry (b)): the files directory
868 // and <host>.env, which installFiles moved into place when nothing
869 // it did not write was there. The database and its marker are
870 // registered by configured, once the install proves the database at
871 // DBPath is this build (registerDatabase): registered here, before
872 // the install, a rollback's retirement of F would park whatever
873 // database stands at DBPath, a foreign one included.
874 paths := []string{host, host + ".env"}
875 if err := ir.local.RecordInventory(ctx, host, captured, "flat", paths, ir.src.ClaimCheck(phase)); err != nil {
876 return false, fmt.Errorf("record the inventory: %w", err)
877 }
878 if err := mark(landStepInventory, ""); err != nil {
879 return false, err
880 }
881 }
882 if !j.Done(landStepTimers) {
883 rows, err := ir.sealedTimers(ctx, seal, src.TimersSHA256)
884 if err != nil {
885 return false, err
886 }
887 if err := cfg.ReplaceTimers(ctx, host, rows); err != nil {
888 return false, fmt.Errorf("replace the timers: %w", err)
889 }
890 if err := mark(landStepTimers, strconv.Itoa(len(rows))); err != nil {
891 return false, err
892 }
893 }
894 if !j.Done(landStepColumns) {
895 if err := cfg.SetReplication(host, src.Replication); err != nil {
896 return false, fmt.Errorf("set replication: %w", err)
897 }
898 if err := cfg.SetNextDue(host, src.NextDue); err != nil {
899 return false, fmt.Errorf("set next_due: %w", err)
900 }
901 if err := mark(landStepColumns, ""); err != nil {
902 return false, err
903 }
904 }
905 if !j.Done(landStepVerify) {
906 if err := cfg.RemoveVerifyEntry(ctx, ir.s, host); err != nil {
907 return false, fmt.Errorf("remove the restore-verification entry: %w", err)
908 }
909 if err := mark(landStepVerify, ""); err != nil {
910 return false, err
911 }
912 }
913 built, err := preparedManifestJournalled(j, hostfiles.ManifestKey(movesFilesPrefix(holder.Location, gen)), id, captured.Stamp)
914 if err != nil {
915 return false, err
916 }
917 if !built {
918 // Spec rev 7.22 §7.6 (L17, R19-1; C8, Task 6): the prepared files
919 // manifest under files/g-<generation>/ in the location, bound to
920 // {location, g-<generation>, op, stamp}, where the generation is the
921 // installation's prepared destination generation (the move id, or
922 // the rollback id of a dirty return), never holder.generation. It
923 // carries the placed files, <host>.env and the wake state the
924 // install put in place above, as create-only objects and one
925 // create-only sequence-0 manifest through the box's own versioned
926 // store, after the claim sync and stamp capture above (op.exec's
927 // installation authority). The binding is journalled with the
928 // manifest's digest; a replay reuses the objects by digest and reads
929 // the manifest back by key. A journal that records the step as
930 // skipped (written before C8 existed) counts as not done (ruling Q3
931 // item 2). Another manifest at the key is not this install's to
932 // replace: the refusal (RollbackManifestOccupied).
933 // The image is the install's own placement, by the install receipt
934 // (files_placed: the tree's identity, the env file's identity or
935 // none): a vanished tree or env file refuses, and the manifest
936 // carries an env item exactly when the archive carried one (fix
937 // round 1, finding 4).
938 placed, err := journalledStaged(j, landStepFilesPlaced)
939 if err != nil {
940 return false, err
941 }
942 if ok, err := sameIdentity(dir, placed.Files); err != nil || !ok {
943 return false, fmt.Errorf("%w: %s is not the files directory this %s placed (%v)", errLandingChanged, dir, ir.src.Name, err)
944 }
945 wantEnv := placed.Env != ""
946 if wantEnv {
947 if ok, err := sameIdentity(envPath, placed.Env); err != nil || !ok {
948 return false, fmt.Errorf("%w: %s is not the env file this %s placed (%v)", errLandingChanged, envPath, ir.src.Name, err)
949 }
950 }
951 rows, err := ir.sealedTimers(ctx, seal, src.TimersSHA256)
952 if err != nil {
953 return false, err
954 }
955 vs, err := ir.versioned(ctx)
956 if err != nil {
957 return false, err
958 }
959 // Every write re-checks the authority first (installAuthority): a
960 // stale build never wins the create-only manifest.
961 authority := installAuthorityStore{VersionedStore: vs, check: ir.installAuthority(ctx, captured, phase)}
962 pm, err := buildPreparedManifest(ctx, authority, cfg.OwnBucket, holder.Location, gen, id, captured.Stamp, dir, envPath, wantEnv, hostfiles.WakeStateOf(host, rows, src.NextDue, src.Replication))
963 if errors.Is(err, hostfiles.ErrManifestExists) {
964 log.Printf("moves: %s: the prepared files manifest of %s is not this install's: %v", host, id, err)
965 return ir.refuse(ctx, refuseManifestOccupied)
966 }
967 if err != nil {
968 return false, fmt.Errorf("build the prepared files manifest: %w", err)
969 }
970 raw, err := json.Marshal(pm)
971 if err != nil {
972 return false, err
973 }
974 if err := mark(landStepFilesManifest, string(raw)); err != nil {
975 return false, err
976 }
977 log.Printf("moves: %s: the prepared files manifest %s is written (sha256 %s)", host, pm.Key, pm.SHA256)
978 }
979 if err := ir.fault("before-files-gen"); err != nil {
980 return false, err
981 }
982 marker, err := json.Marshal(filesGenMarker{Op: gen, Incarnation: holder.Incarnation})
983 if err != nil {
984 return false, err
985 }
986 placed, err := journalledStaged(j, landStepFilesPlaced)
987 if err != nil {
988 return false, err
989 }
990 // The marker goes into the files directory this install placed, and
991 // only while the installation is still this install's (final review,
992 // finding 2): inside the guard, the directory is the receipt's.
993 if err := ir.guarded(ctx, captured, phase, func() error {
994 if ok, err := sameIdentity(dir, placed.Files); err != nil || !ok {
995 return fmt.Errorf("%w: %s is not the files directory this %s placed (%v)", errLandingChanged, dir, ir.src.Name, err)
996 }
997 if err := syncPath(dir); err != nil {
998 return err
999 }
1000 return writeFileAtomic(filepath.Join(dir, archive.MarkerName), marker, 0o644)
1001 }); err != nil {
1002 return false, fmt.Errorf("write %s: %w", archive.MarkerName, err)
1003 }
1004 if err := mark(landStepFilesGen, ""); err != nil {
1005 return false, err
1006 }
1007 log.Printf("moves: %s: files, environment, timers and columns installed; %s written", host, archive.MarkerName)
1008 return ir.advance(ctx, phase, next, installStamp{})
1009}
1010
1011// installArtifactPaths are an installation's artifacts at the flat paths
1012// (G11), relative to the data directory: the database with its marker,
1013// journal, quarantine and litestream shadow, the files directory and
1014// <host>.env. A dirty return parks them before it installs; the source's
1015// finalization parks them and the rendered config beside them.
1016func installArtifactPaths(host string) []string {
1017 db := host + ".db"
1018 return []string{db, db + ".gen", db + "-wal", db + "-shm", db + ".corrupt", ".activator-" + db + "-litestream", host, host + ".env"}
1019}
1020
1021// retirePreMove is a dirty return's local sub-step (2) (ruling J13; fix
1022// round 1): the source's own pre-move installation, and anything an earlier
1023// installation of this holder left on this box, leaves the flat paths before
1024// anything of the return's is placed there, so placeStaged and restore find
1025// them empty and never install over them, and the return's registration
1026// never meets a path tagged with another stamp.
1027//
1028// First the claim's inventory for this box, read fresh each round, is
1029// accounted for stamp by stamp, before anything moves:
1030// - S, the source's installation stamp (the holder's after
1031// ReturnToSource): retired through park.Local under the host guard
1032// against the park-local the source's claimed queued
1033// (RetireArtifacts completes it); paths of S listed with no park-local
1034// owed is an error, since only the claimed CAS queues it;
1035// - a stamp the claim shows superseded on this box (supersededOnBox: a
1036// cold admission here issued it, and it is neither S nor the holder's
1037// live session; spec §7.8, rev 7.11): retired the same way, against a
1038// park-local queued here for it, so its inventory entries clear;
1039// - any other stamp (live, unknown, another holder's): nothing is moved,
1040// and the return stops for an operator (StuckReturnUnknownStamp); a
1041// retry after the operator has accounted for it runs this again.
1042//
1043// A park-local owed with nothing listed (a replay after a retirement) is
1044// completed. Then every pre-move artifact still at the flat paths that no
1045// inventory entry claims is parked by the moves-owned journalled rename
1046// (H5(b), parkLocalArtifacts), its plan and its renames inside the host
1047// guard with the claim and the record read fresh there (fix round 1, ruling
1048// 1), so a call that resumes after authority moved on renames nothing.
1049func (ir *installRun) retirePreMove(ctx context.Context, captured hostguard.Captured, j *moves.Journal, mark func(step, value string) error) (restoreOutcome, error) {
1050 cfg := ir.cfg()
1051 host, id, phase := ir.rec.Host, ir.g.ID, ir.src.Phases[3]
1052 stamp := captured.Stamp
1053 for round := 0; ; round++ {
1054 if round > 64 {
1055 return restoreWaiting, fmt.Errorf("move %s: the source's inventory did not clear", id)
1056 }
1057 obs, err := ir.sync(ctx, phase)
1058 if err != nil {
1059 return restoreWaiting, err
1060 }
1061 c := obs.Claim
1062 var retire []uint64
1063 for _, x := range inventoryStamps(c, cfg.Box) {
1064 switch {
1065 case x == stamp:
1066 retire = append([]uint64{x}, retire...)
1067 case supersededOnBox(c, cfg.Box, x):
1068 retire = append(retire, x)
1069 default:
1070 log.Printf("moves: %s: this box's inventory lists paths under stamp %d, which the return of %s cannot account for; nothing is moved", host, x, id)
1071 ok, err := ir.refuse(ctx, refuseUnknownStamp)
1072 if err != nil || !ok {
1073 return restoreWaiting, err
1074 }
1075 return restoreMarked, nil
1076 }
1077 }
1078 var chore *hostclaim.Obligation
1079 for i := range c.Obligations {
1080 if o := &c.Obligations[i]; o.Op == id && o.Kind == "park-local" && o.Box == cfg.Box {
1081 chore = o
1082 }
1083 }
1084 if len(retire) == 0 {
1085 if chore != nil {
1086 if _, err := hostclaim.Update(ctx, ir.s, obs, hostclaim.CompleteLocalPark(id, cfg.Box, stamp)); err != nil {
1087 return restoreWaiting, fmt.Errorf("complete the source's local park: %w", err)
1088 }
1089 }
1090 break
1091 }
1092 x := retire[0]
1093 if chore == nil {
1094 if x == stamp {
1095 return restoreWaiting, fmt.Errorf("move %s: this box's inventory lists the source's pre-move paths at stamp %d, and the claim owes no park-local for them", id, stamp)
1096 }
1097 if obs, err = hostclaim.Update(ctx, ir.s, obs, hostclaim.QueueLocalPark(id, cfg.Box, obligationID)); err != nil {
1098 return restoreWaiting, fmt.Errorf("queue the local park of stamp %d: %w", x, err)
1099 }
1100 for i := range obs.Claim.Obligations {
1101 if o := &obs.Claim.Obligations[i]; o.Op == id && o.Kind == "park-local" && o.Box == cfg.Box {
1102 chore = o
1103 }
1104 }
1105 if chore == nil {
1106 return restoreWaiting, fmt.Errorf("move %s: the local park of stamp %d was not queued", id, x)
1107 }
1108 }
1109 if err := ir.local.RetireArtifacts(ctx, host, captured, chore.ID, id, x); err != nil {
1110 return restoreWaiting, fmt.Errorf("retire the source's artifacts of stamp %d: %w", x, err)
1111 }
1112 log.Printf("moves: %s: the source's artifacts of stamp %d are retired", host, x)
1113 }
1114 if err := ir.fault("before-" + installStepPreMove); err != nil {
1115 return restoreWaiting, err
1116 }
1117 paths := installArtifactPaths(host)
1118 unclaimed := func(c *hostclaim.Claim) error {
1119 for _, p := range paths {
1120 if _, ok := c.Inventory[cfg.Box].Paths[p]; ok {
1121 return fmt.Errorf("%w: the inventory claims %s", errLandingChanged, p)
1122 }
1123 }
1124 return nil
1125 }
1126 err := ir.guardedWith(ctx, captured, phase, unclaimed, func() error {
1127 _, err := parkLocalArtifacts(ctx, j, mark, localArtifactPark{DataDir: cfg.DataDir, Incarnation: captured.Incarnation, Host: host,
1128 Op: ir.src.GenerationOp, Step: installStepPreMove, Paths: paths, NewID: obligationID, Fault: ir.fault})
1129 return err
1130 })
1131 if err != nil {
1132 return restoreWaiting, fmt.Errorf("park the source's pre-move artifacts: %w", err)
1133 }
1134 return restoreInstalled, nil
1135}
1136
1137// inventoryStamps are the distinct stamps box's inventory tags paths with,
1138// in order.
1139func inventoryStamps(c *hostclaim.Claim, box string) []uint64 {
1140 seen := map[uint64]bool{}
1141 var out []uint64
1142 for _, s := range c.Inventory[box].Paths {
1143 if !seen[s] {
1144 seen[s] = true
1145 out = append(out, s)
1146 }
1147 }
1148 sort.Slice(out, func(i, k int) bool { return out[i] < out[k] })
1149 return out
1150}
1151
1152// supersededOnBox reports whether the claim shows stamp as an installation
1153// stamp this holder issued on box and has since moved past: a cold
1154// admission's stamp is its record's session (hostclaim.AdmitWriter), so a
1155// cold admission record on box with that session names it, and it is
1156// neither the holder's stamp nor its live writer session. A stamp the claim
1157// has no such record of is not this box's to retire.
1158func supersededOnBox(c *hostclaim.Claim, box string, stamp uint64) bool {
1159 h := c.Holder
1160 if h == nil || stamp == 0 || stamp == h.Stamp || stamp == h.Session {
1161 return false
1162 }
1163 for _, a := range h.Admissions {
1164 if a.Mode == "cold" && a.Box == box && a.Session == stamp {
1165 return true
1166 }
1167 }
1168 return false
1169}
1170
1171// releaseLive is the configured step's release (§9.2 as amended
1172// 2026-10-12): ReleaseLive, else, with CARLOS_HOST_CLAIM off, ReleaseReady,
1173// which publishes then. Under the claim T7's hook only stages, so it is
1174// never the fallback: nil, and configured waits.
1175func (c moveCfg) releaseLive() func(context.Context, release.Store, provision.Record, string) moves.Check {
1176 switch {
1177 case c.ReleaseLive != nil:
1178 return c.ReleaseLive
1179 case hostwrite.Enabled(os.Getenv):
1180 return nil
1181 }
1182 return c.ReleaseReady
1183}
1184
1185// configured (§7.6, §9.1, §9.2): the held row rendered (RenderHeld, the
1186// move-bound ack). A landing requires the render's bundle and overlay etags
1187// to be the ones pinned at readiness (else the refusal, "configuration
1188// changed during the move") and the pinned release live and proven for the
1189// held row through releaseLive (ruling G4: "waiting for the root stage
1190// pass" waits, anything else waits until the landing's deadline refuses).
1191// Under the claim (Paul's T7 ruling, 2026-10-12) the render is selected as
1192// held-config and the release is published here, under held-release: T7
1193// only staged it. A
1194// return renders the current inputs instead, never compared with
1195// move.inputs, gates on the release live on this box, and records what it
1196// rendered as Rollback.RecoveryInputs in the CAS that ends the phase
1197// (J2-R2-5). Then the install the brief lists under `restored`: the built
1198// database's sha256 against the manifest, lease.CheckDB, the rename into
1199// DBPath (G34), <dbPath>.gen through hostbox's marker writer naming the
1200// generation (ruling G2), and the CAS to restored.
1201//
1202// A replay finds configured and does it all again: the render and the
1203// release check are idempotent reads and writes of the same bytes; a
1204// database already at DBPath with the manifest's sha256 is this install's
1205// own, so only the marker is written again.
1206func (ir *installRun) configured(ctx context.Context) (bool, error) {
1207 cfg := ir.cfg()
1208 host, id := ir.rec.Host, ir.g.ID
1209 phase, next := ir.src.Phases[4], ir.src.Phases[5]
1210 obs, err := ir.sync(ctx, phase)
1211 if err != nil {
1212 return false, err
1213 }
1214 if obs.Claim.Op.Kind == hostclaim.KindLanding && cfg.registerLanding != nil {
1215 if err := cfg.registerLanding(ctx, obs); err != nil {
1216 return false, err
1217 }
1218 }
1219 var pin, recovery *provision.MoveInputs
1220 if ir.src.Role == roleTarget {
1221 if pin = ir.src.PinInputs; pin == nil {
1222 return false, fmt.Errorf("move %s: the record has no pinned inputs", id)
1223 }
1224 }
1225 rt, err := ir.route()
1226 if err != nil {
1227 return false, err
1228 }
1229 // The render's ownership is journalled before its write (carry (c),
1230 // fix round 1, I1): the hook runs once the rendered bytes are known and
1231 // before they are placed, and journals their sha256 with the file's
1232 // state before the render (pre_render). The rendered sha256 is journalled
1233 // again as soon as the render answers, even when its config ack failed.
1234 // A rollback's cleaning parks <host>.config.env only when its content is
1235 // one this op rendered (rollbackOwnership).
1236 //
1237 // Under the claim the render is selected as held-config, and the pins
1238 // decide first (Task 7, Q2.2): a merge that is not them is the
1239 // configuration-changed refusal, with nothing selected or written.
1240 hr, err := cfg.RenderHeld(ctx, ir.s, rt, ir.src.HoldOp, pin, ir.notePreRender)
1241 if hr.SHA256 != "" {
1242 if jerr := ir.noteRendered(hr.SHA256); jerr != nil {
1243 return false, errors.Join(err, jerr)
1244 }
1245 }
1246 if pin != nil && errors.Is(err, errHeldRenderUnpinned) {
1247 log.Printf("moves: %s: the held render's inputs are not the pinned ones (%v, overlay %q): %v", host, pin.BundleETags, pin.OverlayETag, err)
1248 return ir.refuse(ctx, refuseConfigChanged)
1249 }
1250 if err != nil {
1251 return false, fmt.Errorf("render the held row: %w", err)
1252 }
1253 want := ""
1254 if pin != nil {
1255 if !sameBundle(hr.BundleETags, pin.BundleETags) || hr.OverlayETag != pin.OverlayETag {
1256 log.Printf("moves: %s: the render's etags (%v, overlay %q) are not the pinned ones (%v, overlay %q)", host, hr.BundleETags, hr.OverlayETag, pin.BundleETags, pin.OverlayETag)
1257 return ir.refuse(ctx, refuseConfigChanged)
1258 }
1259 want = pin.Release
1260 } else {
1261 live, err := release.LiveVersion(cfg.OptDir, host)
1262 if err != nil {
1263 return false, fmt.Errorf("read the live release: %w", err)
1264 }
1265 bundle := make(map[string]string, len(hr.BundleETags))
1266 for k, v := range hr.BundleETags {
1267 bundle[k] = v
1268 }
1269 recovery = &provision.MoveInputs{BundleETags: bundle, OverlayETag: hr.OverlayETag, Release: live}
1270 want = live
1271 }
1272 live := cfg.releaseLive()
1273 if live == nil {
1274 ir.w.notice(host+"#release", "moves: %s: release %s cannot be published for the held row: no held-release publication is wired under CARLOS_HOST_CLAIM; retrying", host, want)
1275 return false, nil
1276 }
1277 if c := live(ctx, ir.s, ir.rec, want); !c.OK {
1278 ir.w.notice(host+"#release", "moves: %s: release %s is not live and proven for the held row yet (%s); retrying", host, want, c.Reason)
1279 return false, nil
1280 }
1281 if err := ir.fault("after-release-ready"); err != nil {
1282 return false, err
1283 }
1284 // The render, the release gate and the install can outlast a landing's
1285 // deadline (fix round 1, item 4): it is checked again before the install
1286 // and right before the transition that names it complete.
1287 if ir.pastDeadline() {
1288 return ir.refuse(ctx, refuseDeadline)
1289 }
1290 sha, outcome, err := ir.restore(ctx, obs)
1291 if err != nil {
1292 return false, err
1293 }
1294 switch outcome {
1295 case restoreWaiting:
1296 return false, nil
1297 case restoreMarked:
1298 return true, nil // the record changed: the refusal, not an install
1299 }
1300 if err := ir.registerDatabase(ctx, obs, phase); err != nil {
1301 return false, err
1302 }
1303 if ir.pastDeadline() {
1304 return ir.refuse(ctx, refuseDeadline)
1305 }
1306 return ir.advance(ctx, phase, next, installStamp{InstalledDBSHA256: sha, RecoveryInputs: recovery})
1307}
1308
1309// registerDatabase registers the installed database and its marker under F
1310// (carry (b)), once restore has proved the database at DBPath is this
1311// install's build: renamed into place inside the guard, or found there
1312// with the build's sha256. Journalled, so a replay registers once.
1313func (ir *installRun) registerDatabase(ctx context.Context, obs hostclaim.Observed, phase string) error {
1314 j, err := ir.journal()
1315 if err != nil {
1316 return err
1317 }
1318 if j.Done(landStepInventoryDB) {
1319 return nil
1320 }
1321 captured, err := ir.capture(*obs.Claim.Holder)
1322 if err != nil {
1323 return err
1324 }
1325 host := ir.rec.Host
1326 if err := ir.local.RecordInventory(ctx, host, captured, "flat", []string{host + ".db", host + ".db.gen"}, ir.src.ClaimCheck(phase)); err != nil {
1327 return fmt.Errorf("register the installed database: %w", err)
1328 }
1329 j.Phase = phase
1330 j.Mark(landStepInventoryDB, "")
1331 if err := j.Commit(ir.cfg().DataDir); err != nil {
1332 return err
1333 }
1334 return ir.fault("after-" + landStepInventoryDB)
1335}
1336
1337// configEnvPath is the held render's file, <DataDir>/<host>.config.env.
1338func (ir *installRun) configEnvPath() string {
1339 return filepath.Join(ir.cfg().DataDir, ir.rec.Host+".config.env")
1340}
1341
1342// preRender is the landStepPreRender record: <host>.config.env's state
1343// before the first held render ("absent", "sha256:<hex>" of a regular file,
1344// or "other"), and the intended set: the sha256 of each content a render of
1345// this op may have placed, journalled before its write. The set is pruned
1346// to the file on disk (fix round 4): before each attempt it keeps the hash
1347// of the content now on disk, only when it is already one of the set's, and
1348// adds the new content's. So it never holds more than two hashes, and every
1349// content this op placed that can still be on disk stays named.
1350type preRender struct {
1351 Before string `json:"before"`
1352 Intended []string `json:"intended"`
1353}
1354
1355// notePreRender is the held render's before-write hook. It hashes the file
1356// now on disk (Lstat'ed, never followed) and journals the intended set as
1357// {that hash, if the set already names it} plus {the new content's hash},
1358// before the write. A file whose content the set does not name (the earlier
1359// state, or foreign content) contributes nothing, and the earlier-state
1360// record is kept as first seen. The renderer replaces the file by an atomic
1361// rename (appconfig's renderExecConfigBefore), so the file on disk is always
1362// one whole content: a hash pruned here is never on disk again unless this
1363// op renders it again, when it is journalled again first.
1364func (ir *installRun) notePreRender(sha string) error {
1365 if sha == "" {
1366 return errors.New("moves: the held render names no content")
1367 }
1368 j, err := ir.journal()
1369 if err != nil {
1370 return err
1371 }
1372 path := ir.configEnvPath()
1373 disk := ""
1374 fi, err := os.Lstat(path)
1375 switch {
1376 case errors.Is(err, os.ErrNotExist):
1377 case err != nil:
1378 return err
1379 case fi.Mode().IsRegular():
1380 if disk, err = fileSHA256(path); err != nil {
1381 return err
1382 }
1383 }
1384 var pre preRender
1385 if raw, ok := j.Steps[landStepPreRender]; ok {
1386 if err := json.Unmarshal([]byte(raw), &pre); err != nil {
1387 return fmt.Errorf("the journal's %s step: %w", landStepPreRender, err)
1388 }
1389 } else {
1390 pre.Before = "other"
1391 switch {
1392 case errors.Is(err, os.ErrNotExist) || fi == nil:
1393 pre.Before = "absent"
1394 case disk != "":
1395 pre.Before = disk
1396 }
1397 }
1398 want := "sha256:" + sha
1399 var next []string
1400 if disk != "" && containsString(pre.Intended, disk) {
1401 next = append(next, disk)
1402 }
1403 if want != disk || len(next) == 0 {
1404 next = append(next, want)
1405 }
1406 if strings.Join(next, " ") == strings.Join(pre.Intended, " ") {
1407 return nil
1408 }
1409 pre.Intended = next
1410 body, err := json.Marshal(pre)
1411 if err != nil {
1412 return err
1413 }
1414 j.Mark(landStepPreRender, string(body))
1415 if err := j.Commit(ir.cfg().DataDir); err != nil {
1416 return err
1417 }
1418 return ir.fault("after-" + landStepPreRender)
1419}
1420
1421// noteRendered journals the sha256 (hex) of the file the held render wrote,
1422// as "sha256:<hex>": the latest render's, which is what the file holds.
1423func (ir *installRun) noteRendered(sha string) error {
1424 j, err := ir.journal()
1425 if err != nil {
1426 return err
1427 }
1428 want := "sha256:" + sha
1429 if j.Steps[landStepRendered] == want {
1430 return nil
1431 }
1432 j.Mark(landStepRendered, want)
1433 if err := j.Commit(ir.cfg().DataDir); err != nil {
1434 return err
1435 }
1436 return ir.fault("after-" + landStepRendered)
1437}
1438
1439// recoveryInstallSource binds the installer to a dirty return on the source
1440// (§7.8 source table, the dirty branch; rulings J13, J14, J20): the record
1441// rolling_back and dirty with the source at an install phase; the claim
1442// restoring/claimed for this rollback with this box, the move's source,
1443// executing at the flip's journalled source stamp S; the generation and the
1444// hold named by the rollback id; the return lane moves/<host>/<rb id>/ in
1445// this box's own bucket; the refusal MoveMarkStuck; the phase advance
1446// MoveRollbackSourcePhase, stamping RecoveryInputs at the end of configured.
1447// seal reads the return seal from the claim the phase works from, and
1448// baseline is what it restores to (the return's evidence): both are the
1449// caller's (Plan 4 Tasks 4 and 5). Production does not call it yet.
1450func recoveryInstallSource(w *movesWorker, s release.Store, rec provision.Record, seal func(*hostclaim.Claim) (moves.Seal, error), baseline *provision.MoveSource) (installSource, error) {
1451 cfg := w.cfg
1452 m := rec.Move
1453 if m == nil || m.Legacy() || m.State != provision.MoveRollingBack || m.Rollback == nil || !m.Rollback.Dirty || !moves.ValidOpID(m.Rollback.ID) ||
1454 !strings.HasPrefix(m.Rollback.ID, "rb_") {
1455 return installSource{}, errors.New("moves: only a dirty rollback's source installs a return")
1456 }
1457 if cfg.Box == "" || seal == nil || baseline == nil {
1458 return installSource{}, errors.New("moves: a return's install names this box, its seal and its baseline")
1459 }
1460 g := provision.MoveGuard{ID: m.ID, Incarnation: m.Incarnation}
1461 host, rb, box := rec.Host, m.Rollback.ID, cfg.Box
1462 claimCheck := func(string) func(*hostclaim.Claim) error {
1463 return func(c *hostclaim.Claim) error {
1464 if err := hostclaim.RequireMoveOp(g.ID, hostclaim.KindRestoring, hostclaim.PhaseClaimed)(c); err != nil {
1465 return err
1466 }
1467 if c.Op.Exec != box || len(c.Op.Participants) == 0 || c.Op.Participants[0] != box || c.Op.Intent["source"] != box {
1468 return fmt.Errorf("%w: the executor is %q, not this box as the move's source", hostclaim.ErrWrongState, c.Op.Exec)
1469 }
1470 if c.Op.Intent["rollback"] != rb {
1471 return fmt.Errorf("%w: the op is rollback %q, not %s", hostclaim.ErrWrongState, c.Op.Intent["rollback"], rb)
1472 }
1473 stamp, err := strconv.ParseUint(c.Op.Journal[hostclaim.JournalSourceStamp], 10, 64)
1474 if err != nil || stamp == 0 || c.Holder == nil || c.Holder.Stamp != stamp {
1475 return fmt.Errorf("%w: the holder is not at the source's pre-move stamp", hostclaim.ErrWrongState)
1476 }
1477 return nil
1478 }
1479 }
1480 return installSource{
1481 OpID: g.ID, Host: host, Role: roleSource, Name: "return",
1482 Lane: moves.StagingPrefix(host, rb), Seal: seal, Baseline: baseline,
1483 GenerationOp: rb, HoldOp: rb,
1484 Phases: [6]string{provision.RbSourceParking, provision.RbSourceBuilding, provision.RbSourcePublished,
1485 provision.RbSourceLocal, provision.RbSourceConfigured, provision.RbSourceRestored},
1486 Phase: func(from, to string, st installStamp) func(*provision.Record) error {
1487 var stamp func(*provision.MoveRollback)
1488 if st.RecoveryInputs != nil {
1489 in := st.RecoveryInputs
1490 stamp = func(r *provision.MoveRollback) { r.RecoveryInputs = in }
1491 }
1492 return provision.MoveRollbackSourcePhase(g, from, to, stamp)
1493 },
1494 Sync: func(ctx context.Context) (hostclaim.Observed, error) {
1495 obs, err := hostclaim.Read(ctx, s, host)
1496 if err != nil {
1497 return obs, fmt.Errorf("read the host claim: %w", err)
1498 }
1499 if obs.Absent {
1500 return obs, fmt.Errorf("move %s has no host claim", g.ID)
1501 }
1502 if err := hostclaim.RequireOp(g.ID)(obs.Claim); err != nil {
1503 return obs, fmt.Errorf("move %s: %w", g.ID, err)
1504 }
1505 if h := obs.Claim.Holder; h == nil || h.Incarnation != g.Incarnation {
1506 return obs, fmt.Errorf("move %s: the host claim names another incarnation", g.ID)
1507 }
1508 return obs, nil
1509 },
1510 ClaimCheck: claimCheck,
1511 RecordPhase: func(r *provision.Record) (string, error) {
1512 m := r.Move
1513 if m == nil || m.ID != g.ID || m.Incarnation != g.Incarnation || m.State != provision.MoveRollingBack || m.Rollback == nil || m.Rollback.ID != rb || !m.Rollback.Dirty {
1514 return "", fmt.Errorf("%w: the record is no longer the dirty return of %s", errLandingChanged, rb)
1515 }
1516 return m.Rollback.SourcePhase, nil
1517 },
1518 Refuse: func(ctx context.Context, why installRefusal) (bool, string, error) {
1519 var reason string
1520 switch why {
1521 case refuseFilesOccupied:
1522 reason = provision.StuckReturnFilesOccupied
1523 case refuseManifestOccupied:
1524 reason = provision.StuckReturnManifestOccupied
1525 case refuseInstallMismatch:
1526 reason = provision.StuckReturnInstallMismatch
1527 case refuseUnknownStamp:
1528 reason = provision.StuckReturnUnknownStamp
1529 default:
1530 return false, "", fmt.Errorf("move %s: a return's install has no refusal %d", g.ID, why)
1531 }
1532 var pre provision.Record
1533 after, err := provision.SetRecordMove(ctx, s, rec.Account, rec.App, host, observedMove(&pre, provision.MoveMarkStuck(g, reason)))
1534 if err != nil {
1535 if errors.Is(err, provision.ErrMoveConflict) {
1536 log.Printf("moves: %s: the stuck mark of %s refused (%v); the record moved on", host, rb, err)
1537 return false, "", nil
1538 }
1539 return false, "", fmt.Errorf("mark the return stuck: %w", err)
1540 }
1541 w.logTransition(ctx, s, pre, *after)
1542 log.Printf("moves: %s: the return %s stops for an operator: %s", host, rb, reason)
1543 nudgeSerial(ctx, s)
1544 return true, reason, nil
1545 },
1546 }, nil
1547}
1548