carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package layout
2
3import (
4 "context"
5 "encoding/json"
6 "errors"
7 "fmt"
8 "os"
9 "path/filepath"
10 "sort"
11 "strings"
12 "sync"
13 "time"
14
15 "github.com/carlosframework/platform/internal/activator"
16 "github.com/carlosframework/platform/internal/controlname"
17 "github.com/carlosframework/platform/internal/instancefs"
18 "github.com/carlosframework/platform/internal/provision"
19 "github.com/carlosframework/platform/internal/registry"
20)
21
22// Fence is what the pass needs from the activator (design §8.2 step 2):
23// stop the host and refuse wakes for it, but only when no other operation
24// already holds the host's one fence slot (FenceAndStopIfUnfenced); let it
25// wake again (LiftFence, at exactly the generation this pass installed);
26// and the two reads that decide whether an unproven stop can be trusted.
27type Fence interface {
28 FenceAndStopIfUnfenced(ctx context.Context, host string, generation uint64) (*activator.StopEvidence, error)
29 LiftFence(host string, generation uint64) bool
30 FenceGeneration(host string) uint64
31 Quiesced(host string) bool
32 StopUncertain(host string) bool
33}
34
35// IntentUnreadable is the intent Recover is given when the agent could not
36// read the fleet's layout intent at all, as distinct from "" (no intent was
37// ever recorded). A revert completed under it is held fenced; see
38// recoverHost.
39const IntentUnreadable Mode = "unreadable"
40
41// Pass is the layout pass for one box. It runs one Converge or Recover at
42// a time; the agent's single-flight is what guarantees that.
43type Pass struct {
44 Registry *registry.Registry
45 // Fence is the activator. Nil means there is none (tests): the pass
46 // still journals and moves.
47 Fence Fence
48 DataDir string
49 SocketDir string // "" means provision.SocketDir
50 SocketsGID int // the carlos-sockets group; -1 leaves the group alone
51 // Reload asks routing to pick up a flipped row now rather than on the
52 // next 2s poll. Nil relies on the poll.
53 Reload func()
54 Now func() time.Time
55 Logf func(string, ...any)
56 Generation func() uint64 // monotonic fence generations; default time.Now().UnixNano()
57 // SweptAtStart reports that the agent's startup unit sweep succeeded,
58 // so no instance from an earlier agent can still be running. Without
59 // it an unproven stop is never trusted.
60 SweptAtStart bool
61 // NestedOK is the isolation probe's gate (design §8.5): a box that
62 // cannot establish the namespace does not start nested tenants, so it
63 // does not make any either. Nil means ungated.
64 NestedOK func() error
65 // Serialize, when set, runs the span from journal write to journal
66 // removal under the lock config sync holds, so sync never renders a
67 // host's bundle into the directory the pass is moving it out of.
68 Serialize func(func())
69 // DeferAwake leaves a host that is awake for a later pass, before any
70 // fence is asked for, rather than stopping it to move it. The design
71 // migrates a row that becomes eligible "when it is asleep", and the
72 // agent's serial-move leg runs while tenants are serving; recovery is
73 // not affected, because a host with a journal cannot be woken.
74 //
75 // The deferral is bounded by AwakeGrace. A flat tenant that never
76 // idles would otherwise defer its own migration for as long as it
77 // liked, and while it runs flat it has the agent's uid and so writes
78 // to every nested sibling's raw directory under /data/.instances:
79 // the box is not isolated until no flat tenant remains on it. Once a
80 // host has been seen awake for the grace period, the pass stops it
81 // the way it stops any host (the fence, §8.2 step 2) and moves it; it
82 // wakes nested on its next request.
83 DeferAwake bool
84 // AwakeGrace is how long an awake host is deferred before the pass
85 // stops it anyway; zero means DefaultAwakeGrace.
86 AwakeGrace time.Duration
87 // awakeSince is when each deferred host was first seen awake under
88 // the current direction. In memory only: an agent restart stops every
89 // tenant, so afterwards they are asleep and migrate on the next pass.
90 awakeMu sync.Mutex
91 awakeSince map[string]time.Time
92
93 // kept is the generation of each fence this pass kept installed after
94 // a failed handover, so a later pass knows that fence as its own
95 // rather than another operation's. Cleared when the fence is lifted.
96 keptMu sync.Mutex
97 kept map[string]uint64
98 // held is the reason each host whose nested move was refused after
99 // it had been stopped is kept fenced and down (see migrateHost). It
100 // is cleared when the move goes through, or when the intent turns
101 // flat and the fence is lifted.
102 held map[string]string
103 // notes is the last thing worth telling an operator about each host
104 // that was not a refusal: what a move quarantined or kept aside. It
105 // is an event for status, in memory only; the log carries it too.
106 notes map[string]string
107
108 // abortAt is a test seam: the step name after which a move returns as
109 // if the agent had died there, leaving its journal for Recover.
110 abortAt string
111}
112
113// Result is what the pass says about one host. Outcome is one of
114// migrated, reverted, recovered, unchanged, skipped, deferred, refused or
115// held (refused after the host was stopped for a nested move, and kept
116// fenced and down; see migrateHost). A Result with an empty Host is about
117// the whole pass.
118type Result struct {
119 Host string
120 Outcome string
121 Reason string
122}
123
124// quiesceTimeout bounds one host's stop.
125const quiesceTimeout = 60 * time.Second
126
127// DefaultAwakeGrace is how long an awake host may defer its own move. An
128// hour is longer than any idle window the activator hibernates on, so a
129// tenant that is ever quiet in that time moves while asleep as the design
130// prefers, and one that is never quiet is stopped once, briefly, rather
131// than left flat beside nested siblings for good.
132const DefaultAwakeGrace = time.Hour
133
134// deferAwake reports whether host, found awake, is still within its
135// grace, and how much of the grace is left.
136func (p *Pass) deferAwake(host string) (bool, time.Duration) {
137 grace := p.AwakeGrace
138 if grace <= 0 {
139 grace = DefaultAwakeGrace
140 }
141 p.awakeMu.Lock()
142 defer p.awakeMu.Unlock()
143 if p.awakeSince == nil {
144 p.awakeSince = map[string]time.Time{}
145 }
146 now := p.now()
147 since, seen := p.awakeSince[host]
148 if !seen {
149 p.awakeSince[host] = now
150 return true, grace
151 }
152 if left := grace - now.Sub(since); left > 0 {
153 return true, left
154 }
155 return false, 0
156}
157
158// forgetAwake clears a host's grace clock once the pass has dealt with it
159// in some other way than deferring it.
160func (p *Pass) forgetAwake(host string) {
161 p.awakeMu.Lock()
162 defer p.awakeMu.Unlock()
163 delete(p.awakeSince, host)
164}
165
166func (p *Pass) now() time.Time {
167 if p.Now != nil {
168 return p.Now()
169 }
170 return time.Now()
171}
172
173func (p *Pass) logf(format string, a ...any) {
174 if p.Logf != nil {
175 p.Logf(format, a...)
176 }
177}
178
179func (p *Pass) generation() uint64 {
180 if p.Generation != nil {
181 return p.Generation()
182 }
183 return uint64(time.Now().UnixNano())
184}
185
186func (p *Pass) socketDir() string {
187 if p.SocketDir != "" {
188 return p.SocketDir
189 }
190 return provision.SocketDir
191}
192
193func (p *Pass) reload() {
194 if p.Reload != nil {
195 p.Reload()
196 }
197}
198
199func (p *Pass) serialize(f func()) {
200 if p.Serialize != nil {
201 p.Serialize(f)
202 return
203 }
204 f()
205}
206
207func (p *Pass) layoutInputs(routes []registry.Route) activator.LayoutInputs {
208 return activator.LayoutInputs{
209 DataDir: p.DataDir,
210 SocketDir: p.socketDir(),
211 Journal: func(host string) bool { return JournalPresent(p.DataDir, host) },
212 Peers: func() ([]registry.Route, error) { return routes, nil },
213 }
214}
215
216// Classify is the box's one classification, shared with the status
217// snapshot so both say the same thing about a row.
218func (p *Pass) Classify(rt registry.Route, peers []registry.Route) (activator.Layout, string) {
219 return activator.Classify(rt, p.layoutInputs(peers))
220}
221
222func deadline(ctx context.Context) (context.Context, context.CancelFunc) {
223 return context.WithTimeout(ctx, quiesceTimeout)
224}
225
226// ready is the status a row must have for the pass to move it. Any other
227// status belongs to an operation in flight on the row, the residence
228// move's "moving" among them, and a row about to leave the box is not
229// worth moving.
230func ready(rt registry.Route) bool { return rt.Status == "" || rt.Status == "ready" }
231
232// Converge runs Recover, then moves every managed row that is not already
233// in mode (design §8.2 steps 1 to 9). Rows are visited in host order so a
234// log reads the same way twice. A host Recover reported is not visited
235// again: recovery already converged it, or refused it.
236//
237// Recovery runs first and ungated, so a failing isolation probe never
238// leaves a crashed move fenced; its legs toward nested ask the gate. The
239// fresh moves to nested wait for the gate as a whole. The gate is asked at
240// most once per pass, and both share the answer.
241func (p *Pass) Converge(ctx context.Context, mode Mode) []Result {
242 if mode != ModeNested && mode != ModeFlat {
243 return []Result{{Outcome: "refused", Reason: fmt.Sprintf("layout mode %q is not nested or flat", mode)}}
244 }
245 gate := p.nestedGate()
246 out := p.recover(ctx, mode, gate)
247 if mode == ModeNested {
248 if err := gate(); err != nil {
249 return append(out, Result{Outcome: "refused", Reason: "isolation not ready: " + err.Error()})
250 }
251 }
252 seen := map[string]bool{}
253 for _, r := range out {
254 seen[r.Host] = true
255 }
256 routes, err := p.Registry.List()
257 if err != nil {
258 p.logf("layout: converge: list routes: %v", err)
259 return append(out, Result{Outcome: "refused", Reason: "list routes: " + err.Error()})
260 }
261 sort.Slice(routes, func(i, j int) bool { return routes[i].Host < routes[j].Host })
262 migrated := false
263 for _, rt := range routes {
264 if !activator.ActivatorManaged(rt) || seen[rt.Host] {
265 continue
266 }
267 if ctx.Err() != nil {
268 break
269 }
270 r := p.convergeHost(ctx, rt, routes, mode)
271 migrated = migrated || r.Outcome == "migrated"
272 out = append(out, r)
273 }
274 if migrated {
275 // What no host's move could take (extras.go): a flat app with cwd
276 // / wrote relative paths straight into the data directory, and a
277 // name no row owns cannot be given to one host. Said once per pass
278 // that moved something, for the operator who has to place them.
279 if left := UnattributedEntries(p.DataDir, hostsOf(routes)); len(left) > 0 {
280 p.logf("layout: left in %s, owned by no host on this box: %s. a nested instance cannot see them; move what an app wrote into that app's instance directory by hand", p.DataDir, strings.Join(left, ", "))
281 }
282 }
283 return out
284}
285
286func (p *Pass) convergeHost(ctx context.Context, rt registry.Route, routes []registry.Route, mode Mode) Result {
287 if !ready(rt) {
288 return Result{rt.Host, "deferred", "status " + rt.Status}
289 }
290 layout, reason := p.Classify(rt, routes)
291 switch layout {
292 case activator.LayoutFenced, activator.LayoutConflict:
293 return Result{rt.Host, "refused", reason}
294 case activator.LayoutForeign:
295 return Result{rt.Host, "skipped", "foreign: " + reason}
296 case activator.LayoutNested:
297 if mode == ModeNested {
298 p.forgetAwake(rt.Host)
299 // A host moved before the pass took the app's own entries
300 // with it (#256) has them still in the data directory,
301 // where its namespace cannot see them. Nothing moves here:
302 // the host is running, and what the app has minted since
303 // may be the copy it wants. Said on every pass, so an
304 // operator reading status or the log sees it while it lasts.
305 if left := FlatLeftovers(p.DataDir, rt.Host, hostsOf(routes)); len(left) > 0 {
306 text := fmt.Sprintf("still in %s from before its move, where the instance cannot see them: %s. move what the app needs into %s by hand", p.DataDir, strings.Join(left, ", "), rawInstanceDir(p.DataDir, rt.Host))
307 p.logf("layout: %s: %s", rt.Host, text)
308 p.note(rt.Host, text)
309 }
310 return Result{rt.Host, "unchanged", ""}
311 }
312 case activator.LayoutFlat:
313 if mode == ModeFlat {
314 p.forgetAwake(rt.Host)
315 p.releaseHeld(rt.Host)
316 return Result{rt.Host, "unchanged", ""}
317 }
318 }
319 if p.DeferAwake && p.Fence != nil && !p.Fence.Quiesced(rt.Host) {
320 if within, left := p.deferAwake(rt.Host); within {
321 return Result{rt.Host, "deferred", fmt.Sprintf("awake (stopped for the move in %s unless it sleeps first)", left.Round(time.Second))}
322 }
323 p.logf("layout: %s: awake past its %s grace -> stopping it for the move", rt.Host, p.grace())
324 }
325 p.forgetAwake(rt.Host)
326 return p.migrateHost(ctx, rt.Host, routes, mode)
327}
328
329func (p *Pass) grace() time.Duration {
330 if p.AwakeGrace > 0 {
331 return p.AwakeGrace
332 }
333 return DefaultAwakeGrace
334}
335
336// migrateHost is steps 2 to 9 for one host. It checks what it can before
337// stopping anything, fences and stops the host, and releases it only after
338// the move is committed and routing has reloaded, or has been refused.
339func (p *Pass) migrateHost(ctx context.Context, host string, routes []registry.Route, mode Mode) Result {
340 plan, err := PlanFor(p.DataDir, p.socketDir(), host, mode)
341 if err != nil {
342 return Result{host, "refused", err.Error()}
343 }
344 plan.Logf = p.logf
345 if err := Preflight(plan, routes); err != nil {
346 return Result{host, "refused", err.Error()}
347 }
348 // A host whose database is not on this box cannot move. Finding that
349 // out must not cost it a stop, and the pass directories and a
350 // destination check, on every pass. The same check under the fence
351 // stays the authority.
352 if db := plan.Artifacts[0]; db.Required {
353 if ok, err := lstatPresent(db.Src); err != nil || !ok {
354 return Result{host, "refused", fmt.Sprintf("db is missing at %s", db.Src)}
355 }
356 }
357 r, fence, gen := p.quiesce(ctx, host, p.generation())
358 if fence != fenceOurs {
359 return r
360 }
361 var hold bool
362 r, _, hold = p.shift(host, mode, nil)
363 // Fail closed on the way to nested. A flat host that has been stopped
364 // for its move does not run flat again because a tenant kept the move
365 // refused: a second name for its database, a file in its own future
366 // directory, a symlink at it, each one syscall from inside, and a host
367 // released here would wake flat on its next request and keep its
368 // raw-path reach over its nested siblings for as long as it kept the
369 // refusal alive. So the fence stays, the host stays down, the reason
370 // is kept for status, and every later pass retries from the kept
371 // fence: when the reason is gone the host moves and is released; when
372 // the fleet's intent turns flat, convergeHost lifts the fence and the
373 // host runs flat as asked. Refusals that come from the row (an
374 // operation in flight, an unflag) are not a tenant's doing and lift as
375 // before, so a residence move is never deadlocked on this fence. A
376 // reverse move is never held: a nested host refused its revert is
377 // already isolated, and leaving it running is the safe side.
378 if hold && mode == ModeNested {
379 p.keep(host, gen)
380 p.hold(host, r.Reason)
381 p.logf("layout: %s: held -> %s", host, r.Reason)
382 return Result{host, "held", r.Reason + "; held fenced and down until the reason is cleared or the layout intent is flat"}
383 }
384 p.release(host, gen)
385 return r
386}
387
388// hold records why host is kept fenced and down after a refused nested
389// move; Held is how status reads it. The reason is also written to a hold
390// marker in the layout directory, so an agent restart (every carlos
391// system update) can restore the hold before anything wakes
392// (RestoreHolds) instead of handing the tenant a fresh grace.
393//
394// The marker is uid carlos's file like everything under /data, so a flat
395// tenant can unlink it. That buys the tenant nothing durable: the marker
396// is a hint to re-check, not the authority. At start the pass re-derives
397// the hold from the files themselves (the planted link or symlink is the
398// reason, and it is the thing the tenant would have to remove to be rid
399// of the hold, which is the outcome wanted), so a deleted marker costs
400// one awake grace after one restart, after which the next pass meets the
401// same refusal and holds again. A marker whose reason is gone is dropped
402// at start rather than honoured, for the same reason in reverse.
403func (p *Pass) hold(host, reason string) {
404 p.keptMu.Lock()
405 if p.held == nil {
406 p.held = map[string]string{}
407 }
408 p.held[host] = reason
409 p.keptMu.Unlock()
410 if err := writeHoldMarker(p.DataDir, host, reason, p.now()); err != nil {
411 p.logf("layout: %s: hold marker: %v (the hold stands in memory until the next restart)", host, err)
412 }
413}
414
415// note records an event for status; Note reads it.
416func (p *Pass) note(host, text string) {
417 p.keptMu.Lock()
418 defer p.keptMu.Unlock()
419 if p.notes == nil {
420 p.notes = map[string]string{}
421 }
422 p.notes[host] = text
423}
424
425// Note is the last event the pass recorded for host that was not a
426// refusal: what a move quarantined or kept aside, and when.
427func (p *Pass) Note(host string) string {
428 p.keptMu.Lock()
429 defer p.keptMu.Unlock()
430 return p.notes[host]
431}
432
433// holdMarker is the on-disk record of a hold: <data>/layout/<host>.hold.
434// The suffix is not .json, so JournalHosts and the downgrade guard, which
435// list journals by that suffix, never take a marker for a journal.
436type holdMarker struct {
437 Host string `json:"host"`
438 Reason string `json:"reason"`
439 Since time.Time `json:"since"`
440}
441
442func holdMarkerName(host string) string { return host + ".hold" }
443
444func writeHoldMarker(dataDir, host, reason string, at time.Time) error {
445 if _, err := instancefs.EnsureDirBeneath(dataDir, filepath.Base(journalDir(dataDir)), 0o700); err != nil {
446 return err
447 }
448 raw, err := json.Marshal(holdMarker{Host: host, Reason: reason, Since: at.UTC()})
449 if err != nil {
450 return err
451 }
452 return instancefs.WriteAtomic(journalDir(dataDir), holdMarkerName(host), raw, 0o600)
453}
454
455func removeHoldMarker(dataDir, host string) {
456 if err := instancefs.RemoveBeneath(journalDir(dataDir), holdMarkerName(host)); err != nil && !errors.Is(err, os.ErrNotExist) {
457 // Best effort: a marker that outlives its hold is dropped at the
458 // next start when its reason does not reproduce.
459 _ = err
460 }
461}
462
463// HoldMarkerPresent reports whether host has a hold marker, without reading
464// it. Lstat, like JournalPresent, and an error other than "not exist"
465// counts as present: a marker this process cannot stat is not proof there
466// is none.
467func HoldMarkerPresent(dataDir, host string) bool {
468 _, err := os.Lstat(filepath.Join(journalDir(dataDir), holdMarkerName(host)))
469 return !errors.Is(err, os.ErrNotExist)
470}
471
472// HoldMarkers lists the hosts with a hold marker in the layout directory.
473func HoldMarkers(dataDir string) ([]string, error) {
474 entries, err := os.ReadDir(journalDir(dataDir))
475 if errors.Is(err, os.ErrNotExist) {
476 return nil, nil
477 }
478 if err != nil {
479 return nil, err
480 }
481 var hosts []string
482 for _, e := range entries {
483 if n := e.Name(); !strings.HasPrefix(n, ".") && strings.HasSuffix(n, ".hold") {
484 hosts = append(hosts, strings.TrimSuffix(n, ".hold"))
485 }
486 }
487 return hosts, nil
488}
489
490// RestoreHolds runs at agent start, after journal recovery and before the
491// activator can wake anything. For each hold marker it re-derives the
492// hold from the files: a flat, managed host whose pre-journal checks
493// still refuse for a reason a tenant can plant is fenced again and held,
494// so a restart does not hand it a fresh grace to run flat in. A marker
495// whose reason is gone is removed, and under a flat intent, or none, every
496// marker is removed: the fleet does not want the host nested. An intent
497// that could not be read is treated as nested, the fail-closed side.
498func (p *Pass) RestoreHolds(ctx context.Context, intent Mode) []Result {
499 hosts, err := HoldMarkers(p.DataDir)
500 if err != nil {
501 p.logf("layout: restore holds: %v", err)
502 return []Result{{Outcome: "refused", Reason: "read hold markers: " + err.Error()}}
503 }
504 if len(hosts) == 0 {
505 return nil
506 }
507 sort.Strings(hosts)
508 var out []Result
509 for _, host := range hosts {
510 if intent != ModeNested && intent != IntentUnreadable {
511 removeHoldMarker(p.DataDir, host)
512 out = append(out, Result{host, "released", "hold marker dropped: the layout intent is not nested"})
513 continue
514 }
515 out = append(out, p.restoreHold(ctx, host))
516 }
517 return out
518}
519
520func (p *Pass) restoreHold(ctx context.Context, host string) Result {
521 rt, err := p.Registry.Get(host)
522 if err != nil {
523 return Result{host, "refused", "row: " + err.Error()}
524 }
525 routes, err := p.Registry.List()
526 if err != nil {
527 return Result{host, "refused", "list routes: " + err.Error()}
528 }
529 if rt == nil || !activator.ActivatorManaged(*rt) {
530 removeHoldMarker(p.DataDir, host)
531 return Result{host, "released", "hold marker dropped: the row is gone or no longer managed"}
532 }
533 if layout, _ := p.Classify(*rt, routes); layout != activator.LayoutFlat {
534 removeHoldMarker(p.DataDir, host)
535 return Result{host, "released", "hold marker dropped: the row is " + string(layout)}
536 }
537 reason, plantable := p.recheck(host, routes)
538 if !plantable {
539 removeHoldMarker(p.DataDir, host)
540 if reason == "" {
541 return Result{host, "released", "hold marker dropped: its reason is gone; the next pass moves the host"}
542 }
543 return Result{host, "released", "hold marker dropped: " + reason}
544 }
545 _, fence, gen := p.quiesce(ctx, host, p.generation())
546 if fence != fenceOurs {
547 return Result{host, "deferred", "hold not restored: " + reason}
548 }
549 p.keep(host, gen)
550 p.hold(host, reason)
551 p.logf("layout: %s: held -> %s (restored at start)", host, reason)
552 return Result{host, "held", reason + "; restored at start"}
553}
554
555// recheck runs the pre-journal checks a nested move would run for host
556// and reports the first refusal and whether it is one a tenant can plant
557// (the same classes shift holds on). An empty reason means the move
558// would go ahead.
559func (p *Pass) recheck(host string, routes []registry.Route) (reason string, plantable bool) {
560 plan, err := PlanFor(p.DataDir, p.socketDir(), host, ModeNested)
561 if err != nil {
562 return err.Error(), false
563 }
564 plan.Logf = p.logf
565
566 if err := Preflight(plan, routes); err != nil {
567 return err.Error(), false
568 }
569 if err := EnsureDirs(p.DataDir, p.socketDir(), host, p.SocketsGID); err != nil {
570 return err.Error(), true
571 }
572 if _, err := presence(plan, hostsOf(routes)); err != nil {
573 return err.Error(), true
574 }
575 return "", false
576}
577
578// hostsOf is every host with a row on the box, whatever its kind: the set
579// an entry's owner is chosen from (extras.go).
580func hostsOf(routes []registry.Route) []string {
581 hosts := make([]string, 0, len(routes))
582 for _, rt := range routes {
583 hosts = append(hosts, rt.Host)
584 }
585 return hosts
586}
587
588// Held reports whether the pass keeps host fenced and down after a
589// refused nested move, and why. The fence must still be the pass's own:
590// a record the activator no longer matches is dropped.
591func (p *Pass) Held(host string) (string, bool) {
592 if _, ok := p.keptFence(host); !ok {
593 p.keptMu.Lock()
594 delete(p.held, host)
595 p.keptMu.Unlock()
596 return "", false
597 }
598 p.keptMu.Lock()
599 defer p.keptMu.Unlock()
600 reason, ok := p.held[host]
601 return reason, ok
602}
603
604// releaseHeld lifts a fence the pass kept for host after a refused nested
605// move, because the fleet now wants the host flat.
606func (p *Pass) releaseHeld(host string) {
607 if gen, ok := p.keptFence(host); ok {
608 p.logf("layout: %s: released -> the layout intent is flat; the held fence is lifted", host)
609 p.release(host, gen)
610 }
611}
612
613// fenceState is who holds a host's fence once quiesce returns.
614type fenceState int
615
616const (
617 // fenceOurs: this pass installed the fence and the stop is proven or
618 // trusted, or it is the fence an earlier pass kept after a failed
619 // handover, which has held the host stopped ever since.
620 fenceOurs fenceState = iota
621 // fenceHeld: another operation's fence holds the host; the pass
622 // installed nothing and must lift nothing.
623 fenceHeld
624 // fenceNone: the pass installed nothing, or lifted what it installed,
625 // and the host is not to be touched.
626 fenceNone
627)
628
629// quiesce installs this pass's fence, unless another operation's fence
630// already holds the host, and decides whether the host is proven stopped.
631// The residence move shares the activator's one fence slot per host, so
632// installing over its fence and later lifting ours would reopen admission
633// the move closed; the exclusive install refuses instead, atomically. An unproven stop is trusted only when nothing can be
634// running: no live entry, no stop on record that failed to finish, and a
635// startup sweep that stopped anything an earlier agent left. After an
636// agent restart no asleep host has a stop on record, which is why the
637// entry-less case needs this rather than a proof. Every refusal lifts the
638// fence it installed: a fence left behind would keep a host down with
639// nothing on its way to release it.
640//
641// It returns the generation the host is fenced at when the fence is the
642// pass's: gen for a fence it just installed, or the generation an earlier
643// pass kept after a failed handover, which the caller lifts in the end.
644func (p *Pass) quiesce(ctx context.Context, host string, gen uint64) (Result, fenceState, uint64) {
645 if p.Fence == nil {
646 return Result{}, fenceOurs, gen
647 }
648 fctx, cancel := deadline(ctx)
649 defer cancel()
650 _, err := p.Fence.FenceAndStopIfUnfenced(fctx, host, gen)
651 if err == nil {
652 return Result{}, fenceOurs, gen
653 }
654 if errors.Is(err, activator.ErrFenceHeld) {
655 if kept, ok := p.keptFence(host); ok {
656 return Result{}, fenceOurs, kept
657 }
658 p.logf("layout: %s: deferred -> fenced by another operation", host)
659 return Result{host, "deferred", "fenced by another operation"}, fenceHeld, 0
660 }
661 unproven := errors.Is(err, activator.ErrStopUnproven)
662 if unproven && p.SweptAtStart && p.Fence.Quiesced(host) && !p.Fence.StopUncertain(host) {
663 return Result{}, fenceOurs, gen
664 }
665 p.Fence.LiftFence(host, gen)
666 reason := "stop unproven"
667 if !unproven {
668 reason += ": " + err.Error()
669 }
670 p.logf("layout: %s: deferred -> %s", host, reason)
671 return Result{host, "deferred", reason}, fenceNone, 0
672}
673
674// keep records a fence this pass leaves installed after a failed handover.
675func (p *Pass) keep(host string, gen uint64) {
676 p.keptMu.Lock()
677 defer p.keptMu.Unlock()
678 if p.kept == nil {
679 p.kept = map[string]uint64{}
680 }
681 p.kept[host] = gen
682}
683
684// keptFence reports the generation of a fence this pass kept for host,
685// provided the fence installed now is still exactly that one. A record
686// the activator no longer matches is dropped, so a later fence of another
687// operation is never taken for the pass's own.
688func (p *Pass) keptFence(host string) (uint64, bool) {
689 p.keptMu.Lock()
690 defer p.keptMu.Unlock()
691 gen, ok := p.kept[host]
692 if !ok {
693 return 0, false
694 }
695 if p.Fence == nil || p.Fence.FenceGeneration(host) != gen {
696 delete(p.kept, host)
697 return 0, false
698 }
699 return gen, true
700}
701
702func (p *Pass) release(host string, gen uint64) {
703 if p.Fence != nil {
704 p.Fence.LiftFence(host, gen)
705 }
706 p.keptMu.Lock()
707 defer p.keptMu.Unlock()
708 if p.kept[host] == gen {
709 delete(p.kept, host)
710 if _, held := p.held[host]; held {
711 delete(p.held, host)
712 removeHoldMarker(p.DataDir, host)
713 }
714 }
715}
716
717// shift is steps 3 to 9 for a host this pass already holds fenced and
718// stopped. It re-reads the row under the fence, because the copy the pass
719// classified can be stale: a move cut can have started, or the row been
720// unflagged, re-added or migrated. Every return before the journal is
721// written leaves nothing changed.
722//
723// prior is a completed journal recovery is still holding the host with,
724// or nil. That journal is this pass's own, so it does not count as the
725// host being fenced here. It is replaced by the new journal in one rename
726// (ReplaceJournal), so the host holds one journal or the other at every
727// instant, and a refusal before the handover leaves it held by prior. The
728// handover result says which of those happened.
729//
730// hold is set on a refusal a tenant could have brought about itself from
731// inside the box, with a syscall on files the agent's uid owns: a planted
732// symlink or entry at the instance directory, a planted file at a
733// destination, a second name for a file about to move. A refusal that
734// comes from the row (an operation in flight, an unflag, a route-name
735// collision) or from the pass's own seams is not one, and is not held.
736func (p *Pass) shift(host string, mode Mode, prior *Journal) (res Result, h handover, hold bool) {
737 fresh, err := p.Registry.Get(host)
738 if err != nil {
739 return Result{host, "refused", "row: " + err.Error()}, handoverNone, false
740 }
741 if fresh == nil {
742 return Result{host, "refused", "row vanished under the fence"}, handoverNone, false
743 }
744 if !ready(*fresh) {
745 return Result{host, "refused", "status " + fresh.Status + " under the fence"}, handoverNone, false
746 }
747 if !activator.ActivatorManaged(*fresh) {
748 return Result{host, "refused", "no longer activator-managed under the fence"}, handoverNone, false
749 }
750 routes, err := p.Registry.List()
751 if err != nil {
752 return Result{host, "refused", "list routes: " + err.Error()}, handoverNone, false
753 }
754 want, from := activator.LayoutNested, activator.LayoutFlat
755 if mode == ModeFlat {
756 want, from = activator.LayoutFlat, activator.LayoutNested
757 }
758 in := p.layoutInputs(routes)
759 if prior != nil {
760 in.Journal = func(h string) bool { return h != host && JournalPresent(p.DataDir, h) }
761 }
762 switch layout, reason := activator.Classify(*fresh, in); layout {
763 case want:
764 return Result{host, "unchanged", ""}, handoverNone, false
765 case from:
766 default:
767 return Result{host, "refused", fmt.Sprintf("%s under the fence: %s", layout, reason)}, handoverNone, false
768 }
769 plan, err := PlanFor(p.DataDir, p.socketDir(), host, mode)
770 if err != nil {
771 return Result{host, "refused", err.Error()}, handoverNone, false
772 }
773 plan.Logf = p.logf
774
775 if err := Preflight(plan, routes); err != nil {
776 return Result{host, "refused", err.Error()}, handoverNone, false
777 }
778 // Both directions: a revert renames out of the raw directory, so it
779 // must be a real directory too, and it is cheaper to find a planted
780 // link here than after the journal has fenced the host.
781 if err := EnsureDirs(p.DataDir, p.socketDir(), host, p.SocketsGID); err != nil {
782 return Result{host, "refused", err.Error()}, handoverNone, true
783 }
784 aside, err := DestinationCheck(plan, p.now)
785 if err != nil {
786 return Result{host, "refused", err.Error()}, handoverNone, true
787 }
788 // The presence scan runs inside the section config sync is locked out
789 // of, with the journal write: a bundle rendered between a scan outside
790 // it and the journal would be recorded absent and left at the old path.
791 p.serialize(func() {
792 present, err := presence(plan, hostsOf(routes))
793 if err != nil {
794 res, h, hold = Result{host, "refused", err.Error()}, handoverNone, true
795 return
796 }
797 res, h = p.commit(plan, plan.Journal(present), aside, prior)
798 })
799 if res.Outcome == "migrated" || res.Outcome == "reverted" {
800 p.reload()
801 }
802 return res, h, hold
803}
804
805// presence records which artifacts are at their sources, in artifactNames
806// order, and refuses a plan whose database is not. It then adds to the
807// plan every entry the app made for the host (ScanExtras; hosts is the
808// box's rows, which decide ownership), so the journal written from the
809// plan names them and the move carries them.
810func presence(plan *Plan, hosts []string) ([]bool, error) {
811 present := make([]bool, len(plan.Artifacts))
812 for i, a := range plan.Artifacts {
813 // Nothing is ever renamed for the socket: the listener is gone
814 // with the stopped host, and the next wake binds the new path.
815 if a.Name == "socket" {
816 continue
817 }
818 ok, err := lstatPresent(a.Src)
819 if err != nil {
820 return nil, err
821 }
822 if a.Required && !ok {
823 return nil, fmt.Errorf("%s is missing at %s", a.Name, a.Src)
824 }
825 present[i] = ok
826 plan.Artifacts[i].Present = ok
827 }
828 extras, err := ScanExtras(plan, hosts)
829 if err != nil {
830 return nil, err
831 }
832 if err := plan.AddExtras(extras); err != nil {
833 return nil, err
834 }
835 // With presence known, every file about to move must have one name.
836 // This runs under the same lock as the journal write, so nothing the
837 // agent itself does can add a name between the check and the journal.
838 if err := LinkCheck(plan, func(a Artifact) string { return a.Src }); err != nil {
839 return nil, err
840 }
841 return present, nil
842}
843
844// applyJournal gives a plan rebuilt from the host name what the journal
845// recorded about the move: which fixed artifacts were present, and the
846// extra entries by name. Validate has already checked the names' shape;
847// this checks their ownership against the box's rows as they stand now,
848// because a journal is not an instruction (design §3.3): a forged journal
849// for H naming H-x, where H-x is a host on the box, would otherwise move
850// that host's own directory into H's tree on recovery.
851func applyJournal(plan *Plan, j *Journal, hosts []string) error {
852 for i := range plan.Artifacts {
853 if i < len(j.Artifacts) {
854 plan.Artifacts[i].Present = j.Artifacts[i].Present
855 }
856 }
857 all := append([]string{plan.Host}, hosts...)
858 for _, name := range j.extraNames() {
859 if owner := ownerOf(name, all); owner != plan.Host {
860 return fmt.Errorf("layout: %s: journal names extra %q, which belongs to %s", plan.Host, name, owner)
861 }
862 }
863 return plan.AddExtras(j.extraNames())
864}
865
866// commit is steps 6 to 9 up to the journal's removal. The caller reloads
867// routing afterwards, outside Serialize's lock and still under the fence.
868// Every return after the journal is written leaves it for Recover, and the
869// journal keeps the host fenced until then.
870func (p *Pass) commit(plan *Plan, j *Journal, aside []string, prior *Journal) (Result, handover) {
871 host := plan.Host
872 if prior == nil {
873 if err := WriteJournal(p.DataDir, j); err != nil {
874 return Result{host, "refused", err.Error()}, handoverNone
875 }
876 return p.commitWritten(plan, j, aside), handoverNone
877 }
878 if err := ReplaceJournal(p.DataDir, prior, j); err != nil {
879 // Whatever is on disk now, the pass cannot vouch that it fences
880 // the host, so the caller keeps its own fence instead.
881 return Result{host, "refused", err.Error()}, handoverFailed
882 }
883 return p.commitWritten(plan, j, aside), handoverDone
884}
885
886// handover is what became of the journal recovery held a host with when
887// the converge leg ran.
888type handover int
889
890const (
891 // handoverNone: the leg stopped before the handover; the held journal
892 // is untouched and still fences the host.
893 handoverNone handover = iota
894 // handoverDone: the new journal replaced it; the host is fenced by the
895 // new journal until that move completes.
896 handoverDone
897 // handoverFailed: the replacement itself failed, and what is on disk
898 // is not known to fence the host. The in-memory fence must stay.
899 handoverFailed
900)
901
902// commitWritten is commit after the journal is on disk.
903func (p *Pass) commitWritten(plan *Plan, j *Journal, aside []string) Result {
904 host := plan.Host
905 if p.abortAt == "after-journal" {
906 return Result{host, "refused", "aborted"}
907 }
908 if err := p.moveFiles(plan); err != nil {
909 return Result{host, "refused", err.Error()}
910 }
911 moved, err := VerifyMoved(plan)
912 aside = append(aside, moved...)
913 if err != nil {
914 return Result{host, "refused", notActing(err)}
915 }
916 if p.abortAt == "after-files" {
917 return Result{host, "refused", "aborted"}
918 }
919 if err := UpdatePhase(p.DataDir, j, PhaseRow); err != nil {
920 return Result{host, "refused", err.Error()}
921 }
922 if err := p.Registry.SetInstancePaths(host, plan.From.DB, plan.From.Socket, plan.To.DB, plan.To.Socket); err != nil {
923 return Result{host, "refused", "row: " + err.Error()}
924 }
925 if p.abortAt == "after-cas" {
926 return Result{host, "refused", "aborted"}
927 }
928 if err := UpdatePhase(p.DataDir, j, PhaseDone); err != nil {
929 return Result{host, "refused", err.Error()}
930 }
931 if p.abortAt == "before-remove" {
932 return Result{host, "refused", "aborted"}
933 }
934 if err := RemoveJournal(p.DataDir, host); err != nil {
935 return Result{host, "refused", err.Error()}
936 }
937 outcome := "migrated"
938 if plan.Mode == ModeFlat {
939 outcome = "reverted"
940 }
941 var events []string
942 if extras := plan.ExtraNames(); len(extras) > 0 {
943 events = append(events, "moved with it: "+strings.Join(extras, ", "))
944 }
945 if len(aside) > 0 {
946 events = append(events, "kept aside: "+strings.Join(aside, ", "))
947 }
948 if len(events) > 0 {
949 p.logf("layout: %s: %s -> %s (%s)", host, plan.From.DB, plan.To.DB, strings.Join(events, "; "))
950 p.note(host, fmt.Sprintf("%s at %s: %s", outcome, p.now().UTC().Format(time.RFC3339), strings.Join(events, "; ")))
951 } else {
952 p.logf("layout: %s: %s -> %s", host, plan.From.DB, plan.To.DB)
953 }
954 return Result{host, outcome, ""}
955}
956
957var errAborted = errors.New("layout: aborted by test seam")
958
959// moveFiles renames every artifact the plan records present (from the
960// scan on a first attempt, from the journal on recovery), the app's own
961// entries included. One already at its destination is done; one still at
962// its source is renamed; one at neither is lost, and one at both was not
963// left that way by a crash (the destination check cleared every
964// destination before the journal was written, and a rename leaves a file
965// in exactly one place). Either of the last two is an error that keeps
966// the journal and so the fence.
967func (p *Pass) moveFiles(plan *Plan) error {
968 for _, a := range plan.Artifacts {
969 if a.Name == "socket" || !a.Present {
970 continue
971 }
972 atDst, err := lstatPresent(a.Dst)
973 if err != nil {
974 return err
975 }
976 atSrc, err := lstatPresent(a.Src)
977 if err != nil {
978 return err
979 }
980 switch {
981 case atDst && atSrc:
982 return fmt.Errorf("layout: %s: %s is at both %s and %s; refusing to choose, not acting", plan.Host, a.Name, a.Src, a.Dst)
983 case atDst:
984 continue
985 case !atSrc:
986 return fmt.Errorf("layout: %s: %s is at neither %s nor %s; not acting", plan.Host, a.Name, a.Src, a.Dst)
987 }
988 if err := instancefs.RenameBetween(filepath.Dir(a.Src), filepath.Base(a.Src), filepath.Dir(a.Dst), filepath.Base(a.Dst)); err != nil {
989 return fmt.Errorf("layout: %s: move %s: %w", plan.Host, a.Name, err)
990 }
991 if p.abortAt == "mid-files" {
992 return errAborted
993 }
994 }
995 return nil
996}
997
998// Recover completes or refuses every journal in <data>/layout (design
999// §8.2, "Recovery is the journal"). It runs at agent start and at the top
1000// of every Converge. intent is the fleet's recorded layout: "" when none
1001// was ever recorded, IntentUnreadable when the agent could not read it.
1002// Completing a journal is never gated on the isolation probe, since that
1003// is how a host gets out of the fenced state; a leg after it toward a
1004// nested intent is (see recoverHost).
1005func (p *Pass) Recover(ctx context.Context, intent Mode) []Result {
1006 return p.recover(ctx, intent, p.nestedGate())
1007}
1008
1009func (p *Pass) recover(ctx context.Context, intent Mode, gate func() error) []Result {
1010 names, err := JournalHosts(p.DataDir)
1011 if err != nil {
1012 p.logf("layout: recover: read journals: %v", err)
1013 return []Result{{Outcome: "refused", Reason: "read journals: " + err.Error()}}
1014 }
1015 if len(names) == 0 {
1016 return nil
1017 }
1018 routes, err := p.Registry.List()
1019 if err != nil {
1020 p.logf("layout: recover: list routes: %v", err)
1021 return []Result{{Outcome: "refused", Reason: "list routes: " + err.Error()}}
1022 }
1023 byHost := map[string]registry.Route{}
1024 for _, rt := range routes {
1025 byHost[rt.Host] = rt
1026 }
1027 var out []Result
1028 for _, host := range names {
1029 if ctx.Err() != nil {
1030 break
1031 }
1032 r := p.recoverHost(ctx, host, byHost, intent, gate)
1033 if r.Outcome == "refused" {
1034 p.logf("layout: %s: recovery refused -> %s", host, r.Reason)
1035 }
1036 out = append(out, r)
1037 }
1038 return out
1039}
1040
1041// nestedGate asks NestedOK at most once, on first use, and remembers the
1042// answer for the rest of one pass.
1043func (p *Pass) nestedGate() func() error {
1044 asked := false
1045 var answer error
1046 return func() error {
1047 if p.NestedOK == nil {
1048 return nil
1049 }
1050 if !asked {
1051 asked, answer = true, p.NestedOK()
1052 }
1053 return answer
1054 }
1055}
1056
1057// notActing makes a recovery refusal say, in the words design §8.2 uses,
1058// that the pass left the host alone and fenced.
1059func notActing(err error) string {
1060 if s := err.Error(); strings.HasSuffix(s, "not acting") {
1061 return s
1062 }
1063 return err.Error() + "; not acting"
1064}
1065
1066// recoverHost checks one journal against the row and both derived
1067// endpoints, completes it to its own end under a fence, and then, still
1068// under that fence, converges the host to intent. The host is released
1069// only once it stands at the intent (design §3.3): while the converge leg
1070// runs, the completed journal stays on disk at done and keeps the host
1071// fenced, and if the leg refuses, for any reason, it stays there. A flat
1072// tenant can forge a valid revert journal for a nested sibling; completing
1073// it costs that sibling a round trip, and if the way back is refused the
1074// sibling waits fenced for an operator rather than running flat.
1075//
1076// Completing a journal is never gated on the isolation probe; the leg
1077// after it toward a nested intent is, exactly as Converge(ModeNested) is.
1078// A gate that refuses the leg holds the host like any other refused leg.
1079//
1080// When another operation's fence already holds the host (the residence
1081// move's cut), the pass completes the journal under that fence, installing
1082// and lifting nothing, provided the host has no live entry: the move's own
1083// fence proved or is proving the stop, and the move refuses a host while a
1084// journal exists, so the journal has to be finished for the move to go on.
1085// Without a fence of its own the pass cannot converge the host, so unless
1086// the completed endpoint is already the intent it keeps the journal at
1087// done. The journal then keeps the host fenced if the move aborts and
1088// lifts its fence, and the next recovery converges it.
1089func (p *Pass) recoverHost(ctx context.Context, host string, byHost map[string]registry.Route, intent Mode, gate func() error) Result {
1090 if !controlname.ValidHostShape(host) {
1091 return Result{host, "refused", "journal file name is not a host; not acting"}
1092 }
1093 j, found, err := ReadJournal(p.DataDir, host)
1094 if err != nil {
1095 return Result{host, "refused", notActing(err)}
1096 }
1097 if !found {
1098 return Result{host, "unchanged", ""}
1099 }
1100 rt, ok := byHost[host]
1101 if !ok {
1102 return Result{host, "refused", "journal names a host with no row; not acting"}
1103 }
1104 // Checked before the stop as well as after it, so a journal that
1105 // cannot be honoured never costs its host a stop.
1106 if _, err := Validate(j, p.DataDir, p.socketDir(), rt); err != nil {
1107 return Result{host, "refused", notActing(err)}
1108 }
1109 plan, err := PlanFor(p.DataDir, p.socketDir(), host, j.Mode)
1110 if err != nil {
1111 return Result{host, "refused", notActing(err)}
1112 }
1113 plan.Logf = p.logf
1114
1115 hosts := make([]string, 0, len(byHost))
1116 for h := range byHost {
1117 hosts = append(hosts, h)
1118 }
1119 if err := applyJournal(plan, j, hosts); err != nil {
1120 return Result{host, "refused", notActing(err)}
1121 }
1122 keepFence := false
1123 r, fence, gen := p.quiesce(ctx, host, p.generation())
1124 switch fence {
1125 case fenceNone:
1126 return r
1127 case fenceHeld:
1128 if !p.Fence.Quiesced(host) {
1129 return Result{host, "deferred", "fenced by another operation and not yet stopped"}
1130 }
1131 case fenceOurs:
1132 defer func() {
1133 if !keepFence {
1134 p.release(host, gen)
1135 }
1136 }()
1137 }
1138 // The row was read before the host stopped; check the journal against
1139 // the row as it stands now that nothing else can wake it.
1140 fresh, err := p.Registry.Get(host)
1141 if err != nil {
1142 return Result{host, "refused", notActing(fmt.Errorf("row: %w", err))}
1143 }
1144 if fresh == nil {
1145 return Result{host, "refused", "row vanished under the fence; not acting"}
1146 }
1147 atFrom, err := Validate(j, p.DataDir, p.socketDir(), *fresh)
1148 if err != nil {
1149 return Result{host, "refused", notActing(err)}
1150 }
1151 converge := fence == fenceOurs && (intent == ModeNested || intent == ModeFlat) && intent != j.Mode
1152 var holdReason string
1153 switch {
1154 case fence == fenceHeld && intent != j.Mode:
1155 // Only an instance move shares the fence slot, and its cut waits
1156 // for this journal to go while the pass waits for the move's
1157 // fence to lift. Neither goes on alone, so the reason names both
1158 // ways out for whoever reads the agent log.
1159 holdReason = fmt.Sprintf("completed to %s under an instance move's fence; journal kept at done. the move waits for this journal and the journal waits for the move. abort the move, or remove %s now that the row stands at its destination, and the move can finish", j.Mode, provision.LayoutJournalPath(p.DataDir, host))
1160 case intent == IntentUnreadable && j.Mode == ModeFlat:
1161 // Releasing a completed revert flat before the intent is known is
1162 // the forged-revert window again.
1163 holdReason = "completed to flat; journal kept at done until the layout intent can be read"
1164 }
1165 remove := holdReason == "" && !converge
1166 var res Result
1167 p.serialize(func() { res = p.complete(plan, j, atFrom, remove) })
1168 if res.Outcome != "recovered" {
1169 return res
1170 }
1171 // The row flipped (or was found flipped). A host still held by its
1172 // journal is refused wakes by that, but routing should say where it is.
1173 p.reload()
1174 if holdReason != "" {
1175 p.logf("layout: %s: held -> %s", host, holdReason)
1176 return Result{host, "deferred", holdReason}
1177 }
1178 if !converge {
1179 return res
1180 }
1181 if intent == ModeNested {
1182 if gerr := gate(); gerr != nil {
1183 // Held, not released flat: there is no degraded mode (design
1184 // §8.5), and a host released at a forged revert's endpoint is
1185 // a nested tenant running flat. The journal at done keeps it
1186 // fenced, so the in-memory fence is lifted as usual, and the
1187 // first recovery after the probe passes converges it.
1188 reason := fmt.Sprintf("completed to %s; journal kept at done: not converging to nested: isolation not ready: %v", j.Mode, gerr)
1189 p.logf("layout: %s: held -> %s", host, reason)
1190 return Result{host, "deferred", reason}
1191 }
1192 }
1193 next, h, _ := p.shift(host, intent, j)
1194 switch {
1195 case next.Outcome == "migrated" || next.Outcome == "reverted":
1196 return Result{host, "recovered", fmt.Sprintf("completed to %s, then converged to %s", j.Mode, intent)}
1197 case h == handoverFailed:
1198 // Nothing on disk is known to fence the host, so the pass's own
1199 // fence stays and the host stays down. Releasing it here would run
1200 // it at the completed endpoint, which after a forged revert is a
1201 // nested host running flat. The generation is recorded so the next
1202 // pass knows the fence as its own and runs the leg again.
1203 keepFence = true
1204 p.keep(host, gen)
1205 reason := fmt.Sprintf("completed to %s; handing over to the move to %s failed: %s. the fence is held by this pass after a failed handover; the next pass retries it", j.Mode, intent, next.Reason)
1206 p.logf("layout: %s: refused -> %s", host, reason)
1207 return Result{host, "refused", reason}
1208 case h == handoverNone:
1209 reason := fmt.Sprintf("completed to %s; journal kept at done: converging to %s: %s %s", j.Mode, intent, next.Outcome, next.Reason)
1210 p.logf("layout: %s: held -> %s", host, reason)
1211 return Result{host, "deferred", reason}
1212 }
1213 return Result{host, next.Outcome, fmt.Sprintf("completed to %s; converging to %s: %s", j.Mode, intent, next.Reason)}
1214}
1215
1216// complete runs the journal's remaining phases (design §8.2's recovery
1217// list), and removes the journal only when remove is set; otherwise it
1218// stops at done and the journal keeps holding the host.
1219//
1220// Two states a crash cannot produce are refused rather than acted on. An
1221// artifact the journal records absent that exists at either end: the host
1222// has been fenced since the plan was made, so nothing recorded absent can
1223// have appeared, and moving the rest without it would leave it behind (a
1224// forged journal can use exactly that to strand a database file). And a
1225// done journal whose row is still at from: done is written only after the
1226// row's swap committed, and releasing it would leave a row that names the
1227// files' old place. A journal at row is also checked for every artifact
1228// it records present being at its destination before the row is swapped.
1229func (p *Pass) complete(plan *Plan, j *Journal, atFrom, remove bool) Result {
1230 host, phase := plan.Host, j.Phase
1231 for _, a := range plan.Artifacts {
1232 if a.Name == "socket" || a.Present {
1233 continue
1234 }
1235 for _, path := range []string{a.Src, a.Dst} {
1236 present, err := lstatPresent(path)
1237 if err != nil {
1238 return Result{host, "refused", notActing(err)}
1239 }
1240 if present {
1241 return Result{host, "refused", fmt.Sprintf("journal records %s absent, but it exists at %s; not acting", a.Name, path)}
1242 }
1243 }
1244 }
1245 switch j.Phase {
1246 case PhaseFiles:
1247 if err := p.moveFiles(plan); err != nil {
1248 return Result{host, "refused", notActing(err)}
1249 }
1250 // The same check a first attempt makes before its row flips; the
1251 // plan carries the journal's presence (applyJournal) so the check
1252 // knows which files it moved.
1253 moved, err := VerifyMoved(plan)
1254 if len(moved) > 0 {
1255 p.logf("layout: %s: quarantined on recovery -> %s", host, strings.Join(moved, ", "))
1256 p.note(host, "quarantined "+strings.Join(moved, ", "))
1257 }
1258 if err != nil {
1259 return Result{host, "refused", notActing(err)}
1260 }
1261 if err := UpdatePhase(p.DataDir, j, PhaseRow); err != nil {
1262 return Result{host, "refused", err.Error()}
1263 }
1264 fallthrough
1265 case PhaseRow:
1266 // The row is about to name the destinations, so every file the
1267 // journal records present must be there: a row pointed at a
1268 // missing database has the next wake restore a second one.
1269 for _, a := range plan.Artifacts {
1270 if a.Name == "socket" || !a.Present {
1271 continue
1272 }
1273 present, err := lstatPresent(a.Dst)
1274 if err != nil {
1275 return Result{host, "refused", notActing(err)}
1276 }
1277 if !present {
1278 return Result{host, "refused", fmt.Sprintf("journal records %s present, but it is missing at %s; not acting", a.Name, a.Dst)}
1279 }
1280 }
1281 if atFrom {
1282 if err := p.Registry.SetInstancePaths(host, j.From.DB, j.From.Socket, j.To.DB, j.To.Socket); err != nil {
1283 return Result{host, "refused", notActing(fmt.Errorf("row: %w", err))}
1284 }
1285 }
1286 if err := UpdatePhase(p.DataDir, j, PhaseDone); err != nil {
1287 return Result{host, "refused", err.Error()}
1288 }
1289 case PhaseDone:
1290 if atFrom {
1291 return Result{host, "refused", "journal is done but the row is still at its source; not acting"}
1292 }
1293 default:
1294 return Result{host, "refused", fmt.Sprintf("journal phase %q unknown; not acting", j.Phase)}
1295 }
1296 if remove {
1297 if err := RemoveJournal(p.DataDir, host); err != nil {
1298 return Result{host, "refused", err.Error()}
1299 }
1300 }
1301 p.logf("layout: %s: recovered -> %s (journal phase was %s)", host, j.Mode, phase)
1302 return Result{host, "recovered", string(j.Mode)}
1303}
1304