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 "strconv"
10 "time"
11
12 "github.com/carlosframework/platform/internal/hostbox"
13 "github.com/carlosframework/platform/internal/hostclaim"
14 "github.com/carlosframework/platform/internal/hostguard"
15 "github.com/carlosframework/platform/internal/moves"
16 "github.com/carlosframework/platform/internal/provision"
17 "github.com/carlosframework/platform/internal/registry"
18 "github.com/carlosframework/platform/internal/release"
19)
20
21// The source's recovery after a rollback (moves spec §7.8 source table; Plan
22// 4 Task 5), behind CARLOS_MOVES_PHASE1: the target has released, and the
23// source box takes the host back, clean or dirty, through opened, and then
24// the rolled_back finalization.
25//
26// Routing. MoveRollBack moved the record's region back to From, so the
27// source reaches a rolling_back record through the post-flip participant
28// rule (movesParticipantRecord, J10) in movesPass, whether or not it still
29// has its row. One pass drives it as far as it can, each phase from durable
30// state, under the record's source_phase. A record at X means X's work may
31// have begun (Plan 2's convention, G8; provision's "each is written before
32// its side effects"):
33//
34// - waiting: until the target's evidence, released clean or returned
35// dirty on the record, and the claim restoring/released with nobody
36// executing ("" after the target's evidence, "none" after a
37// declaration). Then the record says claimed.
38// - claimed: the claim's return (hostclaim.ReturnToSource: this box
39// executing at the source's own stamp S; a clean return also names the
40// source's generation, src_gen, again), then the guard's activation
41// (the fresh read finds holder.stamp == S, matching the installation
42// record, or re-initialises a lost one to S), then, for a dirty return
43// whose inventory lists paths of S, the source's park-local (J13: the
44// installer's local retires S against it). The claim's phase is
45// claimed from here through the install (J1).
46// - restoring: the row handed from the move's moving hold to the
47// rollback's restoring hold (handHold, J3), with the record read fresh
48// under the host's hold lock.
49// - clean verified: the files plain digest and the timers digest equal
50// the source's stopped baselines (Source.Files.Plain,
51// Source.TimersSHA256), and the warm database's <dbPath>.gen names
52// src_gen (plain text, H11). A mismatch stops for an operator with the
53// baseline-mismatch sentence at verified (MoveMarkStuck, INV-4); a
54// retry verifies again, never skips it.
55// - dirty parking to configured: the shared installer (installGeneration,
56// Task 3) bound by recoveryInstallSource to the return seal and the
57// return baseline the target journalled in the claim (Task 4), from the
58// return lane moves/<host>/<rb id>/ in this box's own bucket, under S;
59// configured records the inputs it rendered as recovery_inputs, never
60// compared with move.inputs. restored hands over to probing.
61// - probing: the claim steps claimed to probing first (J1), then the
62// recovery probe: recoveryProbe, Task 9's (BLOCKED-ON-CLAIM C1, rulings
63// J2 and J18), a cold admission of kind recovery with nine attempts,
64// into the return generation (dirty, coverage sealed) or a fresh one
65// restored from src_gen (clean, coverage clean-return; J14: both
66// returns are cold and never keep S). The probe records the inputs it
67// renders before it renders them (MoveRecordRecoveryInputs, at
68// probing: recoveryProbeAdapter, movesrecoveryprobe.go, claim Plan 10
69// J2; a clean return records only here). Once the
70// record says probing nothing earlier runs again: a restarted probe
71// restarts only the probe (INV-2, INV-7). Nine failed attempts stop
72// for an operator at probing.
73// - probed: the claim steps to probed and, for a dirty return, queues the
74// return lane's park in this region in the same write
75// (QueueReturnLanePark, its horizon the return seal's bound); then
76// recovered_at, and the serial is nudged (the target's cleaning waits
77// on it).
78// - recovered: the open: the rollback's hold on the row ends
79// (ReleaseHeld, a plain registry call under the host's hold lock with
80// the record read fresh, J12), the claim steps to opened, and the record
81// says opened with opened_at.
82// - opened: the finalization. Once recovered_at, opened_at and
83// target_parked_at are stamped and no obligation of the op remains in
84// the claim, the terminal event (LogEvent, Plan 6's moves.LogEvent),
85// the record's close (MoveRolledBackClose) and then the claim's
86// release to serving (ReleaseMove): record first, ruling M4. Whichever
87// actor stamps last tries; a crash before the record's close is the
88// source's next pass, and one after it the console's recovery pass
89// (Plan 5 Task 7), which releases from LastMove.
90//
91// Every record write goes through a provision transition (the record_seq
92// rule); every claim write through a hostclaim transition.
93//
94// Arming (rulings J18, G17; Plan 5 Task 9a). Like the landing and the
95// target's half of the rollback, the source's recovery needs the claim's
96// park hooks, RunHeld (for the dirty return's residue parks) and the local
97// authorizer, which armMovesParks sets only once the wake-source gate opens
98// (movesWakeSourceRegistered, Plan 5 Task 9b), so
99// recoverySourceArmed answers false in production and the pass logs a
100// standing notice. The probe hook is
101// the recovery probe adapter (armRecoveryProbe) with CARLOS_HOST_CLAIM on,
102// bound to the activator's Group D entry. Without the claim switch the
103// legacy nil hook still waits, recording nothing. LogEvent
104// is nil in production, where the event is a log line.
105
106// recoverStepsPerPass bounds the phases one pass runs: the dirty path from
107// waiting to the finalization is fifteen.
108const recoverStepsPerPass = 16
109
110// recoveryProbeAttempts is the recovery probe's budget (§7.8: three times
111// the landing's three).
112const recoveryProbeAttempts = 9
113
114// The recovery probe's admission coverage (§7.14): a dirty return admits
115// the return generation from its sealed lane, a clean one restores the
116// source's own generation.
117const (
118 recoveryCoverageSealed = "sealed"
119 recoveryCoverageClean = "clean-return"
120)
121
122// recoveryProbeOutcome is what one call of the recovery probe found.
123type recoveryProbeOutcome int
124
125const (
126 // recoveryProbeWaiting: no answer yet; the next pass calls the probe
127 // again (and only the probe).
128 recoveryProbeWaiting recoveryProbeOutcome = iota
129 // recoveryProbeAnswered: the app answered; the recovery goes to probed.
130 recoveryProbeAnswered
131 // recoveryProbeExhausted: every attempt failed; the recovery stops for
132 // an operator (StuckRecoveryProbeAttempts).
133 recoveryProbeExhausted
134)
135
136// recoveryProbeRequest is what the source asks of the recovery probe (Task
137// 9 builds it against claim Group D for recovery, J2).
138type recoveryProbeRequest struct {
139 // RollbackID names the rollback, whose id names a dirty return's
140 // generation and journal.
141 RollbackID string
142 // Clean is the return's kind; Coverage is the admission's coverage,
143 // "sealed" (dirty), "clean-return", or "" for a declared return.
144 Clean bool
145 Coverage string
146 // Declared is a clean return after a declared-lost or fenced target
147 // (Q6): the record's rollback cites the loss the claim journals. It asks
148 // no coverage, for every session, and carries no loss id: the claim's
149 // writer derives it.
150 Declared bool
151 // Generation is the generation the dirty return installed (the
152 // rollback id); empty for a clean return, whose probe admits a fresh
153 // one restored from SourceGeneration (src_gen, J14).
154 Generation, SourceGeneration string
155 // Attempts is the budget, nine.
156 Attempts int
157 // Record records the inputs the probe is about to render, before it
158 // renders them (MoveRecordRecoveryInputs; Task 3b concern 3).
159 Record func(context.Context, provision.MoveInputs) error
160}
161
162// recoverySourceArmed reports whether this box can run the source's
163// recovery: every hook a dirty return's install and the open call is wired,
164// the claim's park hooks, RunHeld and local authorizer included
165// (landingArmed). In production armMovesParks sets those only once the
166// wake-source gate opens (movesWakeSourceRegistered, Plan 5 Task 9b), so this
167// answers false there until then (J18).
168func recoverySourceArmed(cfg moveCfg) bool {
169 return landingArmed(cfg) && cfg.DataDir != "" && cfg.HandHold != nil && cfg.ReleaseHeld != nil && cfg.Timers != nil
170}
171
172// releaseHeld is the recovery's open in production: registry.ReleaseHold
173// under the op that holds the row, then written (holdStageRequester) when
174// it landed, so root re-judges what it staged now the hold has ended. The
175// caller holds provision.WithHostHoldLock.
176func releaseHeld(reg *registry.Registry, written func(host string)) func(host, op string) (bool, error) {
177 return func(host, op string) (bool, error) {
178 released, err := reg.ReleaseHold(host, op)
179 if err == nil && released && written != nil {
180 written(host)
181 }
182 return released, err
183 }
184}
185
186// recoverRun is one pass's state for one rolling_back record on its source.
187type recoverRun struct {
188 w *movesWorker
189 s release.Store
190 rec provision.Record
191 g provision.MoveGuard
192 rb string
193}
194
195// recoverSource is the participant step for a rolling_back record on its
196// source (movesPass).
197func (w *movesWorker) recoverSource(ctx context.Context, s release.Store, rec provision.Record) error {
198 cfg := w.cfg
199 m := rec.Move
200 if m == nil || m.Legacy() || m.State != provision.MoveRollingBack || m.Rollback == nil || m.From != cfg.Region || m.To == cfg.Region {
201 return nil
202 }
203 if !recoverySourceArmed(cfg) {
204 w.notice(rec.Host+"#recover", "moves: %s: the source's recovery of rollback %s waits: this box's recovery 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, m.Rollback.ID)
205 return nil
206 }
207 r := &recoverRun{w: w, s: s, rec: rec, g: provision.MoveGuard{ID: m.ID, Incarnation: m.Incarnation}, rb: m.Rollback.ID}
208 defer r.settleLease()
209 for i := 0; i < recoverStepsPerPass; i++ {
210 advanced, err := r.step(ctx)
211 if err != nil || !advanced {
212 return err
213 }
214 fresh, err := provision.GetRecord(ctx, s, rec.Account, rec.App, rec.Host)
215 if err != nil {
216 return fmt.Errorf("re-read the record: %w", err)
217 }
218 if !r.same(fresh) {
219 return nil
220 }
221 r.rec = *fresh
222 }
223 return nil
224}
225
226func (r *recoverRun) cfg() moveCfg { return r.w.cfg }
227func (r *recoverRun) now() time.Time { return r.w.now() }
228func (r *recoverRun) rollback() *provision.MoveRollback { return r.rec.Move.Rollback }
229func (r *recoverRun) dbPath() string { return provision.DefaultDBPath(r.cfg().DataDir, r.rec.Host) }
230
231// same reports whether fresh is still this rollback, rolling back.
232func (r *recoverRun) same(fresh *provision.Record) bool {
233 m := fresh.Move
234 return m != nil && m.ID == r.g.ID && m.Incarnation == r.g.Incarnation && m.State == provision.MoveRollingBack && m.Rollback != nil && m.Rollback.ID == r.rb
235}
236
237// fault is the test seam after a side effect.
238func (r *recoverRun) fault(point string) error {
239 if f := r.cfg().rcFault; f != nil {
240 return f(point)
241 }
242 return nil
243}
244
245// settleLease releases the own-location lease a dirty return's build holds
246// from building through published, once the record is past them or no
247// longer this return: the lease is the build's alone.
248func (r *recoverRun) settleLease() {
249 m := r.rec.Move
250 if m != nil && m.State == provision.MoveRollingBack && m.Rollback != nil && m.Rollback.Dirty &&
251 (m.Rollback.SourcePhase == provision.RbSourceBuilding || m.Rollback.SourcePhase == provision.RbSourcePublished) {
252 return
253 }
254 releaseOwnLease(r.w, r.rec.Host)
255}
256
257// step runs the source's current recovery phase once. advanced is true when
258// it wrote the record.
259func (r *recoverRun) step(ctx context.Context) (bool, error) {
260 rb := r.rollback()
261 switch rb.SourcePhase {
262 case provision.RbSourceWaiting:
263 return r.waiting(ctx)
264 case provision.RbSourceClaimed:
265 return r.claimed(ctx)
266 case provision.RbSourceRestoring:
267 return r.restoring(ctx)
268 case provision.RbSourceVerified:
269 return r.verified(ctx)
270 case provision.RbSourceParking, provision.RbSourceBuilding, provision.RbSourcePublished, provision.RbSourceLocal, provision.RbSourceConfigured:
271 return r.install(ctx)
272 case provision.RbSourceRestored:
273 return r.phase(ctx, provision.RbSourceRestored, provision.RbSourceProbing, nil, "")
274 case provision.RbSourceProbing:
275 return r.probing(ctx)
276 case provision.RbSourceProbed:
277 return r.probed(ctx)
278 case provision.RbSourceRecovered:
279 return r.open(ctx)
280 case provision.RbSourceOpened:
281 return r.finalize(ctx)
282 }
283 return false, nil
284}
285
286// phase writes one source phase step, logs it (movesevents.go names it from
287// the record; the last argument, the step's old event name, is unused) and
288// passes the step's fault point. A conflict is someone else's decision.
289func (r *recoverRun) phase(ctx context.Context, from, to string, stamp func(*provision.MoveRollback), _ string) (bool, error) {
290 host := r.rec.Host
291 var pre provision.Record
292 after, err := provision.SetRecordMove(ctx, r.s, r.rec.Account, r.rec.App, host, observedMove(&pre, provision.MoveRollbackSourcePhase(r.g, from, to, stamp)))
293 if err != nil {
294 if errors.Is(err, provision.ErrMoveConflict) {
295 log.Printf("moves: %s: the source's recovery %s to %s refused (%v); the record moved on", host, from, to, err)
296 return false, nil
297 }
298 return false, fmt.Errorf("the source's recovery %s to %s: %w", from, to, err)
299 }
300 log.Printf("moves: %s: the source of rollback %s is %s", host, r.rb, to)
301 r.w.logTransition(ctx, r.s, pre, *after)
302 if err := r.fault("after-record-" + to); err != nil {
303 return true, err
304 }
305 return true, nil
306}
307
308// stuck stops the recovery for an operator (MoveMarkStuck) with a recorded
309// reason; the row stays held and move --retry returns to the phase.
310func (r *recoverRun) stuck(ctx context.Context, reason string, why error) (bool, error) {
311 host := r.rec.Host
312 log.Printf("moves: %s: the recovery of %s stops for an operator (%v): %s", host, r.rb, why, reason)
313 var pre provision.Record
314 after, err := provision.SetRecordMove(ctx, r.s, r.rec.Account, r.rec.App, host, observedMove(&pre, provision.MoveMarkStuck(r.g, reason)))
315 if err != nil {
316 if errors.Is(err, provision.ErrMoveConflict) {
317 log.Printf("moves: %s: the stuck mark of %s refused (%v); the record moved on", host, r.rb, err)
318 return false, nil
319 }
320 return false, fmt.Errorf("mark the recovery stuck: %w", err)
321 }
322 r.w.logTransition(ctx, r.s, pre, *after)
323 r.w.pageStuck(ctx, r.rec, "source", reason)
324 nudgeSerial(ctx, r.s)
325 return true, nil
326}
327
328// claim reads the host claim and requires this rollback's restoring op,
329// with this box the move's source, for the record's incarnation.
330func (r *recoverRun) claim(ctx context.Context) (hostclaim.Observed, error) {
331 obs, err := hostclaim.Read(ctx, r.s, r.rec.Host)
332 if err != nil {
333 return obs, fmt.Errorf("read the host claim: %w", err)
334 }
335 if obs.Absent {
336 return obs, fmt.Errorf("move %s has no host claim", r.g.ID)
337 }
338 if err := r.ours(obs.Claim); err != nil {
339 return obs, fmt.Errorf("move %s: %w", r.g.ID, err)
340 }
341 return obs, nil
342}
343
344// ours is the claim check of every source phase before the finalization:
345// this rollback's restoring op, this box the move's source, the record's
346// incarnation.
347func (r *recoverRun) ours(c *hostclaim.Claim) error {
348 box := r.cfg().Box
349 if c == nil || c.Op.ID != r.g.ID || c.Op.Kind != hostclaim.KindRestoring || c.Op.Intent["rollback"] != r.rb {
350 return fmt.Errorf("%w: the claim is not rollback %s", hostclaim.ErrWrongState, r.rb)
351 }
352 if c.Holder == nil || c.Holder.Incarnation != r.g.Incarnation {
353 return fmt.Errorf("%w: the claim names another incarnation", hostclaim.ErrWrongState)
354 }
355 if len(c.Op.Participants) == 0 || c.Op.Participants[0] != box || c.Op.Intent["source"] != box {
356 return fmt.Errorf("%w: this box is not the move's source in the claim", hostclaim.ErrWrongState)
357 }
358 return nil
359}
360
361// executing requires the claim at phase with this box executing.
362func (r *recoverRun) executing(c *hostclaim.Claim, phase string) error {
363 if c.Op.Phase != phase || c.Op.Exec != r.cfg().Box {
364 return fmt.Errorf("move %s: the claim is restoring/%q with executor %q, not %s on this box", r.g.ID, c.Op.Phase, c.Op.Exec, phase)
365 }
366 return nil
367}
368
369// sourceStamp is S, the source installation's stamp the flip journalled.
370func sourceStamp(c *hostclaim.Claim) (uint64, error) {
371 s, err := strconv.ParseUint(c.Op.Journal[hostclaim.JournalSourceStamp], 10, 64)
372 if err != nil || s == 0 {
373 return 0, fmt.Errorf("%w: the flip's source stamp is not journalled", hostclaim.ErrWrongState)
374 }
375 return s, nil
376}
377
378// stillAt is the record check run under the host's hold lock before a row
379// write: the record, read fresh, must still be this rollback with the
380// source at phase. It reads the record only.
381func (r *recoverRun) stillAt(ctx context.Context, phase string) error {
382 fresh, err := provision.GetRecord(ctx, r.s, r.rec.Account, r.rec.App, r.rec.Host)
383 if err != nil {
384 return fmt.Errorf("re-read the record: %w", err)
385 }
386 if !r.same(fresh) || fresh.Move.Rollback.SourcePhase != phase {
387 return fmt.Errorf("move %s: the record is no longer rollback %s at %s", r.g.ID, r.rb, phase)
388 }
389 return nil
390}
391
392// waiting (§7.8 source waiting): the target's evidence, released clean or
393// returned dirty, and the claim released with nobody executing.
394func (r *recoverRun) waiting(ctx context.Context) (bool, error) {
395 host, rb := r.rec.Host, r.rollback()
396 if !(rb.TargetPhase == provision.RbTargetReleased && !rb.Dirty) && !(rb.TargetPhase == provision.RbTargetReturned && rb.Dirty) {
397 r.w.notice(host+"#rc-wait", "moves: %s: the source waits for the target of rollback %s (%s, dirty %t)", host, r.rb, rb.TargetPhase, rb.Dirty)
398 return false, nil
399 }
400 obs, err := r.claim(ctx)
401 if err != nil {
402 return false, err
403 }
404 if op := obs.Claim.Op; op.Phase != hostclaim.PhaseReleased || (op.Exec != "" && op.Exec != "none") {
405 r.w.notice(host+"#rc-wait", "moves: %s: the source waits for the claim's release of rollback %s (restoring/%q, executor %q)", host, r.rb, op.Phase, op.Exec)
406 return false, nil
407 }
408 return r.phase(ctx, provision.RbSourceWaiting, provision.RbSourceClaimed, nil, "")
409}
410
411// claimed (§7.8 source claimed): the claim's return to this box at S (and
412// src_gen for a clean return), the guard's activation at S, and a dirty
413// return's park-local for its registered pre-move installation (J13).
414func (r *recoverRun) claimed(ctx context.Context) (bool, error) {
415 cfg := r.cfg()
416 host, id, rb := r.rec.Host, r.g.ID, r.rollback()
417 clean := !rb.Dirty
418 if err := r.markJournals(); err != nil {
419 return false, err
420 }
421 obs, err := r.claim(ctx)
422 if err != nil {
423 return false, err
424 }
425 if op := obs.Claim.Op; op.Phase != hostclaim.PhaseClaimed || op.Exec != cfg.Box {
426 if obs, err = hostclaim.Update(ctx, r.s, obs, hostclaim.ReturnToSource(id, cfg.Box, clean, hostclaim.ReturnFence{})); err != nil {
427 return false, fmt.Errorf("the claim's return to the source: %w", err)
428 }
429 log.Printf("moves: %s: the source executes the host again for rollback %s (clean %t)", host, r.rb, clean)
430 }
431 if err := r.fault("after-return-claimed"); err != nil {
432 return false, err
433 }
434 c := obs.Claim
435 s, err := sourceStamp(c)
436 if err != nil {
437 return false, err
438 }
439 if c.Holder.Stamp != s {
440 return false, fmt.Errorf("move %s: the holder is at stamp %d, not the source's %d", id, c.Holder.Stamp, s)
441 }
442 if clean && c.Holder.Generation != c.Op.Journal[hostclaim.JournalSrcGen] {
443 return false, fmt.Errorf("move %s: a clean return's holder names generation %q, not src_gen", id, c.Holder.Generation)
444 }
445 g := cfg.Guard
446 if g.Claims == nil {
447 g.Claims = hostguard.StoreReader{Store: r.s}
448 }
449 if err := g.Activate(ctx, host, cfg.Box, nil); err != nil {
450 return false, fmt.Errorf("activate the source's installation at stamp %d: %w", s, err)
451 }
452 if err := r.fault("after-activate"); err != nil {
453 return false, err
454 }
455 if rb.Dirty && hostclaim.InventoryHasStamp(c, cfg.Box, s) {
456 if _, err := hostclaim.Update(ctx, r.s, obs, hostclaim.QueueLocalPark(id, cfg.Box, obligationID)); err != nil {
457 return false, fmt.Errorf("queue the source's local park: %w", err)
458 }
459 }
460 return r.phase(ctx, provision.RbSourceClaimed, provision.RbSourceRestoring, nil, "")
461}
462
463// restoring (§7.8 source restoring): the row handed from the move's hold to
464// the rollback's, as restoring (J3), with the record checked under the
465// host's hold lock; then the branch, clean or dirty.
466func (r *recoverRun) restoring(ctx context.Context) (bool, error) {
467 cfg := r.cfg()
468 host, id := r.rec.Host, r.g.ID
469 handed, err := cfg.HandHold(host, "restoring", r.rb, id, func() error { return r.stillAt(ctx, provision.RbSourceRestoring) })
470 if err != nil {
471 return false, fmt.Errorf("hand the row's hold to the rollback: %w", err)
472 }
473 if !handed {
474 return false, fmt.Errorf("move %s: this box's row is held by neither the move nor rollback %s", id, r.rb)
475 }
476 if err := r.fault("after-hand"); err != nil {
477 return false, err
478 }
479 next := provision.RbSourceVerified
480 if r.rollback().Dirty {
481 next = provision.RbSourceParking
482 }
483 return r.phase(ctx, provision.RbSourceRestoring, next, nil, "")
484}
485
486// verified (§7.8 clean verified, INV-4): the source's files and timers hash
487// to what it recorded when it stopped, and its warm database's marker names
488// src_gen. A mismatch stops for an operator; a retry runs this again.
489func (r *recoverRun) verified(ctx context.Context) (bool, error) {
490 cfg := r.cfg()
491 host, id, m := r.rec.Host, r.g.ID, r.rec.Move
492 if err := r.fault("verify"); err != nil {
493 return false, err
494 }
495 obs, err := r.claim(ctx)
496 if err != nil {
497 return false, err
498 }
499 if err := r.executing(obs.Claim, hostclaim.PhaseClaimed); err != nil {
500 return false, err
501 }
502 srcGen, ok := obs.Claim.Op.Journal[hostclaim.JournalSrcGen]
503 if !ok {
504 return false, fmt.Errorf("move %s: the flip's source generation is not journalled", id)
505 }
506 if m.Source == nil || m.Source.Files == nil || m.Source.Files.Plain == "" || m.Source.TimersSHA256 == "" {
507 return false, fmt.Errorf("move %s: the record has no stopped baselines to verify against", id)
508 }
509 dir, envPath := cfg.FilesDir(host)
510 plain, _, _, err := archivePlainDigest(dir, envPath)
511 why, retry, ferr := archiveFailure(host, err)
512 switch {
513 case ferr != nil:
514 return false, fmt.Errorf("digest the source's files: %w", ferr)
515 case retry:
516 r.w.notice(host+"#rc-verify", "moves: %s: a file changed while the source's files were digested; the verification runs again", host)
517 return false, nil
518 case why != "":
519 return r.stuck(ctx, provision.StuckBaselineMismatch, fmt.Errorf("the source's files cannot be digested: %s", why))
520 case plain != m.Source.Files.Plain:
521 return r.stuck(ctx, provision.StuckBaselineMismatch, fmt.Errorf("the files are %s, recorded %s", plain, m.Source.Files.Plain))
522 }
523 rows, err := cfg.Timers(host)
524 if err != nil {
525 return false, fmt.Errorf("read the timers: %w", err)
526 }
527 if got := moves.TimersDigest(rows); got != m.Source.TimersSHA256 {
528 return r.stuck(ctx, provision.StuckBaselineMismatch, fmt.Errorf("the timers are %s, recorded %s", got, m.Source.TimersSHA256))
529 }
530 if why, err := r.warmMismatch(srcGen); err != nil {
531 return false, err
532 } else if why != nil {
533 return r.stuck(ctx, provision.StuckBaselineMismatch, why)
534 }
535 log.Printf("moves: %s: the source still matches what it recorded when it stopped", host)
536 return r.phase(ctx, provision.RbSourceVerified, provision.RbSourceProbing, nil, "")
537}
538
539// warmMismatch is the warm database's half of the clean verification (fix
540// round 1, ruling 1). A marker that is there must name src_gen. A database
541// with no marker is refused unless src_gen is the origin (""), whose
542// database never had one. A database evicted with its marker passes: the
543// probe restores the source cold from src_gen (J14), so nothing warm is
544// read. why is the mismatch; err a read that failed.
545func (r *recoverRun) warmMismatch(srcGen string) (why, err error) {
546 db := r.dbPath()
547 gen, gerr := hostbox.ReadGenerationMarker(db + ".gen")
548 if gerr != nil {
549 return fmt.Errorf("the warm database's marker: %w", gerr), nil
550 }
551 present, err := regularFile(db)
552 if err != nil {
553 return nil, err
554 }
555 switch {
556 case gen != "" && gen != srcGen:
557 return fmt.Errorf("the warm database's marker names %q, the pre-move generation is %q", gen, srcGen), nil
558 case gen == "" && present && srcGen != "":
559 return fmt.Errorf("the warm database has no marker, the pre-move generation is %q", srcGen), nil
560 case gen == "" && !present:
561 log.Printf("moves: %s: the source's warm database was evicted; the probe restores generation %q cold", r.rec.Host, srcGen)
562 }
563 return nil, nil
564}
565
566// returnSeal reads the return seal the target journalled in the claim.
567func returnSeal(c *hostclaim.Claim) (moves.Seal, error) {
568 raw, ok := c.Op.Journal[moves.JournalReturnSeal]
569 if !ok {
570 return moves.Seal{}, fmt.Errorf("%w: the claim carries no return seal", moves.ErrBadSeal)
571 }
572 return moves.DecodeSeal(raw)
573}
574
575// install (§7.8 dirty parking to configured): one phase of the shared
576// installer over the return lane, its seal and baseline the target's
577// journalled return. A refusal it records stops the return for an operator.
578func (r *recoverRun) install(ctx context.Context) (bool, error) {
579 host := r.rec.Host
580 obs, err := r.claim(ctx)
581 if err != nil {
582 return false, err
583 }
584 raw, ok := obs.Claim.Op.Journal[moves.JournalReturnBaseline]
585 if !ok {
586 return false, fmt.Errorf("move %s: the claim carries no return baseline", r.g.ID)
587 }
588 baseline, err := moves.DecodeReturnBaseline(raw)
589 if err != nil {
590 return false, err
591 }
592 src, err := recoveryInstallSource(r.w, r.s, r.rec, returnSeal, &baseline)
593 if err != nil {
594 return false, err
595 }
596 advanced, refused, err := installGeneration(ctx, r.s, r.w, r.rec, src)
597 if refused != "" {
598 log.Printf("moves: %s: the return of %s stopped for an operator: %s", host, r.rb, refused)
599 // The stuck mark is installGeneration's write, logged there.
600 r.w.pageStuck(ctx, r.rec, "source", refused)
601 }
602 return advanced, err
603}
604
605// probing (§7.8 source probing): the claim steps to probing before the
606// first attempt (J1), then the recovery probe. Nothing earlier is run again
607// from here.
608func (r *recoverRun) probing(ctx context.Context) (bool, error) {
609 cfg := r.cfg()
610 host, id, rb := r.rec.Host, r.g.ID, r.rollback()
611 obs, err := r.claim(ctx)
612 if err != nil {
613 return false, err
614 }
615 if obs.Claim.Op.Phase == hostclaim.PhaseClaimed {
616 if err := r.executing(obs.Claim, hostclaim.PhaseClaimed); err != nil {
617 return false, err
618 }
619 if obs, err = hostclaim.Update(ctx, r.s, obs, hostclaim.SetRestoringPhase(id, hostclaim.PhaseClaimed, hostclaim.PhaseProbing)); err != nil {
620 return false, fmt.Errorf("the claim's probing: %w", err)
621 }
622 if err := r.fault("after-claim-probing"); err != nil {
623 return false, err
624 }
625 }
626 if err := r.executing(obs.Claim, hostclaim.PhaseProbing); err != nil {
627 return false, err
628 }
629 if cfg.recoveryProbe == nil {
630 r.w.notice(host+"#rc-probe", "moves: %s: the recovery probe of rollback %s waits: it is Task 9's, on claim Plan 10's recovery admission", host, r.rb)
631 return false, nil
632 }
633 req := recoveryProbeRequest{RollbackID: r.rb, Clean: !rb.Dirty, Attempts: recoveryProbeAttempts,
634 SourceGeneration: obs.Claim.Op.Journal[hostclaim.JournalSrcGen],
635 Record: func(ctx context.Context, in provision.MoveInputs) error {
636 _, err := provision.SetRecordMove(ctx, r.s, r.rec.Account, r.rec.App, host, provision.MoveRecordRecoveryInputs(r.g, r.rb, in))
637 return err
638 }}
639 // A declared or fenced return asks no coverage (Q6); while the record
640 // and the claim disagree on the target's loss the probe waits.
641 declared, err := recoveryDeclared(obs.Claim, rb.LossRef)
642 if err != nil {
643 r.w.notice(host+"#rc-probe", "moves: %s: the recovery probe of rollback %s waits: %v", host, r.rb, err)
644 return false, nil
645 }
646 switch {
647 case rb.Dirty:
648 req.Coverage, req.Generation = recoveryCoverageSealed, r.rb
649 case declared:
650 req.Declared = true
651 default:
652 req.Coverage = recoveryCoverageClean
653 }
654 out, err := cfg.recoveryProbe(ctx, r.s, r.rec, req)
655 var missing *ClaimDependencyError
656 if errors.As(err, &missing) {
657 // The adapter refused before recording or waking (Plan 10 J2):
658 // probing waits, as it did with no hook.
659 r.w.notice(host+"#rc-probe", "moves: %s: the recovery probe of rollback %s waits: %v", host, r.rb, err)
660 return false, nil
661 }
662 if err != nil {
663 return false, fmt.Errorf("the recovery probe: %w", err)
664 }
665 switch out {
666 case recoveryProbeAnswered:
667 return r.phase(ctx, provision.RbSourceProbing, provision.RbSourceProbed, nil, "")
668 case recoveryProbeExhausted:
669 return r.stuck(ctx, provision.StuckRecoveryProbeAttempts, fmt.Errorf("%d attempts", recoveryProbeAttempts))
670 }
671 r.w.notice(host+"#rc-probe", "moves: %s: the recovery probe of rollback %s has not answered yet", host, r.rb)
672 return false, nil
673}
674
675// probed (§7.8 source probed): the claim steps to probed and, for a dirty
676// return, queues the return lane's park in the same write; then recovered
677// with recovered_at, and the serial is nudged for the target's cleaning.
678func (r *recoverRun) probed(ctx context.Context) (bool, error) {
679 cfg := r.cfg()
680 host, id, rb := r.rec.Host, r.g.ID, r.rollback()
681 obs, err := r.claim(ctx)
682 if err != nil {
683 return false, err
684 }
685 var lane func(*hostclaim.Claim) error
686 if rb.Dirty {
687 seal, err := returnSeal(obs.Claim)
688 if err != nil {
689 return false, err
690 }
691 scope := moves.ReturnLaneScope(host, r.rb, obs.Claim.Holder.Incarnation, seal.Bound())
692 lane = hostclaim.QueueReturnLanePark(id, cfg.parkRegion(), scope, obligationID)
693 }
694 switch obs.Claim.Op.Phase {
695 case hostclaim.PhaseProbing:
696 if err := r.executing(obs.Claim, hostclaim.PhaseProbing); err != nil {
697 return false, err
698 }
699 step := hostclaim.SetRestoringPhase(id, hostclaim.PhaseProbing, hostclaim.PhaseProbed)
700 if _, err := hostclaim.Update(ctx, r.s, obs, func(c *hostclaim.Claim) error {
701 if err := step(c); err != nil {
702 return err
703 }
704 if lane != nil {
705 return lane(c)
706 }
707 return nil
708 }); err != nil {
709 return false, fmt.Errorf("the claim's probed: %w", err)
710 }
711 case hostclaim.PhaseProbed:
712 // A replay after the claim's write: the lane park is idempotent,
713 // and once the park engine completed it it is not queued again.
714 if err := r.executing(obs.Claim, hostclaim.PhaseProbed); err != nil {
715 return false, err
716 }
717 if lane != nil {
718 if _, err := hostclaim.Update(ctx, r.s, obs, lane); err != nil {
719 return false, fmt.Errorf("queue the return lane's park: %w", err)
720 }
721 }
722 default:
723 return false, r.executing(obs.Claim, hostclaim.PhaseProbing)
724 }
725 if err := r.fault("after-claim-probed"); err != nil {
726 return false, err
727 }
728 ok, err := r.phase(ctx, provision.RbSourceProbed, provision.RbSourceRecovered, func(rb *provision.MoveRollback) {
729 t := r.now().UTC()
730 rb.RecoveredAt = &t
731 }, "source-recovered")
732 if ok {
733 nudgeSerial(ctx, r.s)
734 }
735 return ok, err
736}
737
738// open (§7.8 source opened): the rollback's hold on the row ends, under the
739// host's hold lock with the record read fresh (J12); the claim steps to
740// opened; the record says opened with opened_at. A replay that finds the
741// row already open goes on.
742func (r *recoverRun) open(ctx context.Context) (bool, error) {
743 cfg := r.cfg()
744 host, id := r.rec.Host, r.g.ID
745 var err error
746 provision.WithHostHoldLock(host, func() {
747 if err = r.stillAt(ctx, provision.RbSourceRecovered); err != nil {
748 return
749 }
750 var released bool
751 if released, err = cfg.ReleaseHeld(host, r.rb); err != nil || released {
752 return
753 }
754 var rt *registry.Route
755 if rt, err = cfg.Route(host); err != nil {
756 return
757 }
758 switch {
759 case rt == nil:
760 err = fmt.Errorf("move %s: this box has no row to open", id)
761 case rt.HoldOp != "":
762 err = fmt.Errorf("move %s: the row is held %s by %q, not by rollback %s", id, rt.Status, rt.HoldOp, r.rb)
763 }
764 })
765 if err != nil {
766 return false, fmt.Errorf("open the row: %w", err)
767 }
768 if err := r.fault("after-row-opened"); err != nil {
769 return false, err
770 }
771 obs, err := r.claim(ctx)
772 if err != nil {
773 return false, err
774 }
775 if obs.Claim.Op.Phase != hostclaim.PhaseOpened {
776 if err := r.executing(obs.Claim, hostclaim.PhaseProbed); err != nil {
777 return false, err
778 }
779 if _, err := hostclaim.Update(ctx, r.s, obs, hostclaim.SetRestoringPhase(id, hostclaim.PhaseProbed, hostclaim.PhaseOpened)); err != nil {
780 return false, fmt.Errorf("the claim's opened: %w", err)
781 }
782 }
783 if err := r.fault("after-claim-opened"); err != nil {
784 return false, err
785 }
786 return r.phase(ctx, provision.RbSourceRecovered, provision.RbSourceOpened, func(rb *provision.MoveRollback) {
787 t := r.now().UTC()
788 rb.OpenedAt = &t
789 }, "source-opened")
790}
791
792// finalize (§7.8 rolled_back): with the source opened and recovered and the
793// target parked, and no obligation of the op left in the claim, the
794// terminal event, the record's close, then the claim's release to serving:
795// record first (§7.5 and §19 K8 as Paul's ruling M4 amends them). A crash
796// between the halves leaves the record at rest and the claim
797// restoring/opened with nothing owed; the console's recovery pass releases
798// from LastMove. A claim already released by this move (a binary that
799// closed claim first) only closes.
800func (r *recoverRun) finalize(ctx context.Context) (bool, error) {
801 cfg := r.cfg()
802 host, id, rb := r.rec.Host, r.g.ID, r.rollback()
803 if rb.RecoveredAt == nil || rb.OpenedAt == nil || rb.TargetParkedAt == nil {
804 r.w.notice(host+"#rc-final", "moves: %s: rollback %s waits for the target's park before it closes", host, r.rb)
805 return false, nil
806 }
807 obs, err := hostclaim.Read(ctx, r.s, host)
808 if err != nil {
809 return false, fmt.Errorf("read the host claim: %w", err)
810 }
811 if obs.Absent {
812 return false, fmt.Errorf("move %s has no host claim", id)
813 }
814 released := hostclaim.RequireReleasedMove(id)(obs.Claim) == nil
815 if !released {
816 if err := r.ours(obs.Claim); err != nil {
817 return false, fmt.Errorf("move %s: %w", id, err)
818 }
819 if err := r.executing(obs.Claim, hostclaim.PhaseOpened); err != nil {
820 return false, err
821 }
822 // Nothing owed under the op, checked before the record's close
823 // (M4), as the console's grant checks it.
824 if owed := owedUnder(obs.Claim, id); owed > 0 {
825 r.w.notice(host+"#rc-final", "moves: %s: rollback %s waits for %d chore(s) of the move to be acknowledged before it closes", host, r.rb, owed)
826 return false, nil
827 }
828 }
829 var pre provision.Record
830 closed, err := provision.SetRecordMove(ctx, r.s, r.rec.Account, r.rec.App, host, observedMove(&pre, provision.MoveRolledBackClose(r.g, r.now())))
831 if err != nil {
832 if errors.Is(err, provision.ErrMoveConflict) {
833 log.Printf("moves: %s: the close of rollback %s refused (%v); the record moved on", host, r.rb, err)
834 return false, nil
835 }
836 return false, fmt.Errorf("close the rolled-back move: %w", err)
837 }
838 r.w.logTransition(ctx, r.s, pre, *closed)
839 log.Printf("moves: %s: move %s is rolled back and closed", host, id)
840 r.w.pageRolledBack(ctx, r.rec)
841 nudgeSerial(ctx, r.s)
842 if !released {
843 if err := r.fault("after-close-record"); err != nil {
844 return true, err
845 }
846 if err := releaseAfterClose(ctx, r.s, host, id, hostclaim.ReleaseMove(id, cfg.Box)); err != nil {
847 return true, err
848 }
849 log.Printf("moves: %s: the claim is serving again after rollback %s", host, r.rb)
850 if err := r.fault("after-release-move"); err != nil {
851 return true, err
852 }
853 }
854 if err := r.fault("after-close"); err != nil {
855 return true, err
856 }
857 // The journals go in the pass that closes; a crash before this leaves
858 // them to the local journal sweep (sweepRecoveryJournal).
859 for _, op := range []string{r.rb, id} {
860 if err := parkRecoveryJournal(cfg.DataDir, host, op, true); err != nil {
861 return true, fmt.Errorf("park the source's journal of %s: %w", op, err)
862 }
863 }
864 return true, nil
865}
866
867// The source's recovery steps in its two op journals, the cut's under the
868// move id and the recovery's under the rollback id (§7.8: "journaled ... in
869// <DataDir>/.moves/<host>/<rollback id>/"; a dirty return's install keeps
870// its build and extract scratch there too). Fix round 1, ruling 2.
871const (
872 // rcStepSource marks a journal as a source recovery's (written at
873 // claimed): sourceJournalMark, so the sweep can tell it from a cut, a
874 // landing or a target's, and knows the incarnation to park it under.
875 rcStepSource = "rb-source"
876 // rcStepClosed is "done" once the record closed.
877 rcStepClosed = "rb-source-closed"
878 // rcStepJournalPark is parkLocalArtifacts' step: the retirement the
879 // journal is parked into (no artifacts of its own).
880 rcStepJournalPark = "rb-source-journal-park"
881)
882
883// sourceJournalMark is rcStepSource's value.
884type sourceJournalMark struct {
885 Move string `json:"move"`
886 Rollback string `json:"rollback"`
887 Incarnation string `json:"incarnation"`
888}
889
890// markJournals marks the source's journals as this recovery's: the cut's,
891// when this box holds it, and the recovery's own, made here for a clean
892// return (the installer's for a dirty one). A mark already there is kept.
893func (r *recoverRun) markJournals() error {
894 dd, host := r.cfg().DataDir, r.rec.Host
895 body, err := json.Marshal(sourceJournalMark{Move: r.g.ID, Rollback: r.rb, Incarnation: r.g.Incarnation})
896 if err != nil {
897 return err
898 }
899 for _, op := range []string{r.g.ID, r.rb} {
900 if op == r.g.ID && !moveJournalPresent(dd, host, op) {
901 continue
902 }
903 j, err := moves.ReadJournal(dd, host, op)
904 if err != nil {
905 return err
906 }
907 if j.Done(rcStepSource) {
908 continue
909 }
910 j.Mark(rcStepSource, string(body))
911 if err := j.Commit(dd); err != nil {
912 return err
913 }
914 }
915 return nil
916}
917
918// parkRecoveryJournal parks one source journal of a closed rollback: closed
919// (true when the caller has seen the close) is journalled first, then a
920// moves-owned retirement with no artifacts is made for it (its receipt names
921// the journal's op) and the journal is renamed into it. A journal that is
922// not there was parked already; one with no recovery mark is not this
923// recovery's and is left.
924func parkRecoveryJournal(dataDir, host, op string, closed bool) error {
925 if !moveJournalPresent(dataDir, host, op) {
926 return nil
927 }
928 j, err := moves.ReadJournal(dataDir, host, op)
929 if err != nil {
930 return err
931 }
932 var mark sourceJournalMark
933 raw, ok := j.Steps[rcStepSource]
934 if !ok {
935 return nil
936 }
937 if err := json.Unmarshal([]byte(raw), &mark); err != nil || !journalComponent(mark.Incarnation) {
938 return fmt.Errorf("the journal of %s has an unreadable recovery mark", op)
939 }
940 write := func(step, value string) error {
941 j.Mark(step, value)
942 return j.Commit(dataDir)
943 }
944 if closed && j.Steps[rcStepClosed] != "done" {
945 if err := write(rcStepClosed, "done"); err != nil {
946 return err
947 }
948 }
949 retirement, err := parkLocalArtifacts(context.Background(), j, write, localArtifactPark{DataDir: dataDir, Incarnation: mark.Incarnation,
950 Host: host, Op: op, Step: rcStepJournalPark, NewID: obligationID})
951 if err != nil {
952 return err
953 }
954 if err := parkOpJournal(dataDir, mark.Incarnation, host, op, retirement); err != nil {
955 return err
956 }
957 log.Printf("moves: %s: the source's journal of %s is parked", host, op)
958 return nil
959}
960
961// sweepRecoveryJournal is the local journal sweep's source-recovery half
962// (sweepFinalJournal, fix round 1, ruling 2): a journal the recovery marked
963// (rcStepSource) is parked once the close is shown by durable evidence:
964// - the journal records the close (rcStepClosed "done");
965// - no listed record carries the move: a rolling_back or stuck move is
966// cleared only by provision.MoveRolledBackClose, which refuses unless
967// the rollback's recovered_at, opened_at and target_parked_at are all
968// stamped (the aborts clear only an aborting move, MoveComplete only a
969// cutting one), and the mark is written while the record is rolling
970// back, so the close landed;
971// - a listed record carries the move with exactly those three stamps on
972// this rollback: the close's preconditions hold, nothing of the
973// recovery reads its journals again, and the close follows.
974//
975// A host the listing does not name, or a record that carries the move
976// without the three stamps, is left.
977func (w *movesWorker) sweepRecoveryJournal(dataDir, host, op string, j *moves.Journal, recs []provision.Record) error {
978 var mark sourceJournalMark
979 if err := json.Unmarshal([]byte(j.Steps[rcStepSource]), &mark); err != nil || mark.Move == "" || mark.Rollback == "" {
980 return fmt.Errorf("the journal of %s has an unreadable recovery mark", op)
981 }
982 closed := j.Steps[rcStepClosed] == "done"
983 if !closed {
984 if len(recs) == 0 {
985 w.notice(host+"#rc-sweep-"+op, "moves: %s: the source's journal of %s is a recovery's, and no listed record names the host; it is left", host, op)
986 return nil
987 }
988 for _, rec := range recs {
989 m := rec.Move
990 if m == nil || m.ID != mark.Move {
991 continue
992 }
993 rb := m.Rollback
994 if rb == nil || rb.ID != mark.Rollback || rb.RecoveredAt == nil || rb.OpenedAt == nil || rb.TargetParkedAt == nil {
995 return nil
996 }
997 return parkRecoveryJournal(dataDir, host, op, false)
998 }
999 }
1000 return parkRecoveryJournal(dataDir, host, op, true)
1001}
1002