carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1// wakeplan.go is the activator's half of the rollout scheduler (zero-downtime
2// deploys spec §6.4, §8.2): which build a wake boots, the first-wake budget,
3// the fallback to the good build, and the 10 s observation. All of it is
4// reached through Service.Planner, which only CARLOS_SWAP=overlap sets. With
5// Planner nil (the flag off) nothing here runs.
6package activator
7
8import (
9 "context"
10 "errors"
11 "fmt"
12 "log"
13 "sync"
14 "time"
15
16 "github.com/carlosframework/platform/internal/registry"
17 "github.com/carlosframework/platform/internal/release"
18 "github.com/carlosframework/platform/internal/rolloutwords"
19)
20
21// WakePlan is the scheduler's answer to "which build does this wake boot"
22// (spec §8.2). The zero plan keeps today's wake.
23type WakePlan struct {
24 // Boot is the artifact to exec, by its immutable staged path.
25 Boot release.StagedIdentity
26 Mode SwapMode
27 // FirstWake: Boot has never served on this instance. The wake is a canary
28 // for it, with the 55 s budget and the 10 s observation.
29 FirstWake bool
30 // Fallback is the good build, booted when a first wake in overlap mode
31 // fails. Nil when there is none.
32 Fallback *release.StagedIdentity
33 // Refuse, when set, fails the wake before the lease with this reason.
34 Refuse string
35 // FailedUnder is the PromotedAt of the pointer Boot was promoted under,
36 // read by the planner before the wake runs, so a failed first wake is
37 // published with no blocking read (spec §8.1), as dispatch.FailedUnder
38 // is for an attempt. Zero: the failure's own time stands in.
39 FailedUnder time.Time
40 // Check is the planner's selection gate (spec §7.4, §8.1–8.4): nil when
41 // build may still boot. Planning runs before the lease and the restore,
42 // so a sibling's failure recorded since can hold the build it picked.
43 // The activator asks it at every planned exec, after the restore; the
44 // good build passes unless a hold build has run on the instance since it
45 // last passed (the scheduler's hold-ran marker), where nothing falls
46 // back. Nil: nothing to re-check. It is called without a.mu held.
47 Check func(build string) error
48 // Token identifies this planned wake (final review F4). The planner
49 // assigns it, unique per plan it makes (a replan, checkPlan's, is a new
50 // plan with a new token), before it registers anything for the wake.
51 // The activator never makes or changes one: it copies the token of the
52 // plan the wake ran, unchanged, into every GenEvent of that wake,
53 // afterWake's and the first-wake observer's, so the scheduler can act
54 // only on the wake it registered. Zero: the planner matches nothing by
55 // token.
56 Token uint64
57 // ConfigRev is the config revision the planner snapshotted for this
58 // wake: the delivered revision to activate once the wake serves. The
59 // activator does not know which revision a wake started with (it reads
60 // the instance's env file at the spawn), so it carries this one,
61 // unchanged, into every GenEvent of the wake, beside Token.
62 ConfigRev string
63}
64
65func (p WakePlan) planned() bool { return p.Boot.StagedPath != "" }
66
67// errPlanStale: the planned build is no longer the one to boot. A
68// supersession (gateSuperseded), so it records nothing and falls back to
69// nothing (classifyStep's stepStale).
70var errPlanStale = fmt.Errorf("the planned build is no longer the one to boot: %w", errSwapSuperseded)
71
72// checkPlan is a planned wake's selection gate before its spawn (spec §7.4,
73// §8.1–8.4), after the lease and the restore: a refused plan is planned
74// again once, on the state already restored, and a second refusal abandons
75// the wake (errPlanStale). It returns the plan to boot. The exec itself asks
76// the returned plan's Check once more (planGate), with no second replan.
77// The planner and Check are called without a.mu held.
78func (a *Service) checkPlan(ctx context.Context, e *actEntry, rt registry.Route, plan WakePlan) (WakePlan, error) {
79 if plan.Check == nil {
80 return plan, nil
81 }
82 err := plan.Check(plan.Boot.Version)
83 if err == nil {
84 return plan, nil
85 }
86 if a.Planner == nil {
87 return plan, fmt.Errorf("%w: %v", errPlanStale, err)
88 }
89 next := a.Planner(ctx, rt)
90 if next.Refuse != "" || !next.planned() {
91 return plan, fmt.Errorf("%w: %v; replanned: %s", errPlanStale, err, next.Refuse)
92 }
93 if next.Check != nil {
94 if nerr := next.Check(next.Boot.Version); nerr != nil {
95 // The replan never becomes the wake's plan, so afterWake will
96 // report only the first plan: the replan's own terminal event
97 // is sent here (rollout Task 5). Nothing of it ran. A planner
98 // that handed out the same token again gets one event for it.
99 if next.Token != plan.Token {
100 a.cancelPlanned(rt, next, false)
101 }
102 return plan, fmt.Errorf("%w: %v", errPlanStale, nerr)
103 }
104 }
105 a.mu.Lock()
106 e.plan, e.firstWake = next, next.FirstWake
107 a.mu.Unlock()
108 // The replaced plan's build never ran (its check refused it before the
109 // exec), and afterWake now reports the replan: the replaced plan's
110 // terminal event is sent here (rollout Task 5).
111 if plan.Token != next.Token {
112 a.cancelPlanned(rt, plan, false)
113 }
114 // The replanned build (and its fallback) is published before it execs
115 // (spec §4.2, §8.5). A failed write execs neither: the wake fails with
116 // errStagedBuildNotPublished, which is abandonment, never a failure.
117 if err := a.publishBeforeExec(); err != nil {
118 return next, err
119 }
120 return next, nil
121}
122
123// wakeOwned is the ownership check at a wake's exec (spec §7.4): the wake's
124// lifecycle has not ended, and plan 3's ownedLocked holds for a waking
125// entry (the entry is the host's, no fence, no shutdown; its lease is the
126// wake's own, which planGate checks beside it).
127func (a *Service) wakeOwned(ctx context.Context, e *actEntry) error {
128 if err := ctx.Err(); err != nil {
129 return err
130 }
131 a.mu.Lock()
132 defer a.mu.Unlock()
133 return a.ownedLocked(e, true)
134}
135
136// planGate is the gate every planned exec passes, B's and its fallback's
137// alike, asked immediately before the exec (startPinnedAt's gate, or
138// startWorker's before-exec hook): the wake's locally held lease
139// (leaseLost; the entry has no replica yet, so ownedLocked cannot see it),
140// the ownership check (wakeOwned), and the plan's selection gate for the
141// build about to exec. Every refusal is abandonment, never a failure of the
142// build: the lease and ownership refusals are gateNoOwnership, the
143// selection gate's is errPlanStale (gateSuperseded), a cancelled wake is
144// the lifecycle's. Check runs after a.mu is released.
145func (a *Service) planGate(ctx context.Context, e *actEntry, lease Lease, plan WakePlan, build string) func() error {
146 return func() error {
147 if err := leaseLost(lease); err != nil {
148 return err
149 }
150 if err := a.wakeOwned(ctx, e); err != nil {
151 return err
152 }
153 if plan.Check != nil {
154 if err := plan.Check(build); err != nil {
155 return fmt.Errorf("%w: %v", errPlanStale, err)
156 }
157 }
158 return nil
159 }
160}
161
162func genOf(id release.StagedIdentity) Gen {
163 return Gen{Build: id.Version, StagedPath: id.StagedPath, Digest: id.Digest, Demand: id.Demand}
164}
165
166// workerGenOf is a worker's generation as the scheduler sees it: its
167// captured staged artifact, marked Worker, with no number. The scheduler
168// compares a worker's generations by their artifact (rollout's
169// acknowledged), because a respawn on the same entry changes the build
170// without a number of its own.
171func workerGenOf(id release.StagedIdentity) Gen {
172 g := genOf(id)
173 g.Worker = true
174 return g
175}
176
177// withIdentity is g, the generation a planned exec started, carrying the
178// staged identity it was booted from (PF10, spec §12 "Legacy sidecar
179// binding"): the build, its staged path, digest and demand. The legacy
180// sidecar co-spawned with it execs that staged path (startSidecar), and the
181// scheduler records it as good by its digest.
182func withIdentity(g Gen, id release.StagedIdentity) Gen {
183 g.Build, g.StagedPath, g.Digest, g.Demand = id.Version, id.StagedPath, id.Digest, id.Demand
184 return g
185}
186
187// GenEventKind names what happened to a planned wake.
188type GenEventKind string
189
190const (
191 GenHealthy GenEventKind = "healthy" // the planned build is serving
192 GenSettled GenEventKind = "settled" // a first wake survived its 10 s
193 GenFailed GenEventKind = "failed" // the planned build failed, nothing fell back
194 GenFellBack GenEventKind = "fellback" // the planned build failed, the good build serves
195 // GenCancelled: a first wake's observation ended without a verdict (an
196 // intentional stop, spec §6.4). It records nothing about the build; it
197 // only closes the observation GenHealthy opened.
198 GenCancelled GenEventKind = "cancelled"
199)
200
201// GenEvent is a planned wake's outcome, for the scheduler.
202type GenEvent struct {
203 Kind GenEventKind
204 Route registry.Route
205 Gen Gen // the generation serving (healthy, settled, fellback)
206 Failed release.StagedIdentity // the build that failed (failed, fellback)
207 Mode SwapMode
208 FirstWake bool
209 Reason string
210 At time.Time
211 // FailedUnder is the plan's FailedUnder (failed, fellback): the
212 // failing pointer's PromotedAt, read before the wake ran.
213 FailedUnder time.Time
214 // Token and ConfigRev are those of the plan the wake ran
215 // (WakePlan.Token, WakePlan.ConfigRev; final review F4). Every event of
216 // one planned wake carries the same pair, on every kind.
217 Token uint64
218 ConfigRev string
219 // Executed (cancelled only): the first wake's build ran, or may have,
220 // before the wake ended with no verdict, and Gen.Build names it (final
221 // review wave 2, N2). Always so for a cancelled observation, which
222 // follows a healthy wake. A hold first wake that its lifecycle ended
223 // after the exec (a stop, a fence, a failure after the spawn) is
224 // reported the same way: the build may have migrated the database, so
225 // the scheduler must not boot the good build there next.
226 Executed bool
227}
228
229// The first-wake budget (spec §8.2). Vars so tests can shrink them.
230var (
231 firstWakeBudget = 55 * time.Second
232 firstWakeKillReserve = 25 * time.Second
233 firstWakeKillBound = 2 * time.Second
234 firstWakeSettle = 10 * time.Second
235 // firstWakeMinVerdict is the least health window B is exec'd with on a
236 // first wake that has a good build. B's window is fixed from the wake's
237 // start, so a slow lease, restore or seeding eats it; below this floor
238 // a failure would be the clock's, not the build's, and the good build
239 // boots instead with nothing recorded against B.
240 firstWakeMinVerdict = 20 * time.Second
241 // outcomeFenceWait bounds how long a new wake waits for a planned wake's
242 // outcome to be delivered before it plans (final review F3). OnGen's
243 // work is in memory plus one local failure-book write, so it normally
244 // returns in milliseconds; 5 s covers a slow disk with room to spare and
245 // still leaves the waiting requests most of their wakeWaitBound (30 s).
246 // It bounds the waiting request, never the restriction (final review
247 // wave 2, N1): past it the wake is refused before it plans, and the
248 // fence stays until the outcome is delivered. A later request retries.
249 outcomeFenceWait = 5 * time.Second
250)
251
252// genKiller (plan 3 Task 2, opset.go) is plan 2's generation kill
253// (ExecBackend.KillGen and its SplitBackend forward): cgroup.kill on
254// generation n's child cgroup, then wait until it is empty. This file uses
255// it and declares none.
256
257// Current is the generation an awake instance serves, for the scheduler: a
258// worker's is its captured spawn build (workerGenOf). A first wake under its
259// 10 s observation reads as not awake, so the scheduler leaves it alone
260// until the observation ends.
261func (a *Service) Current(host string) (Gen, bool) {
262 a.mu.Lock()
263 defer a.mu.Unlock()
264 e := a.entries[host]
265 if e == nil || e.state != actAwake || e.firstWatch != nil {
266 return Gen{}, false
267 }
268 if e.worker {
269 return workerGenOf(e.workerServing), true
270 }
271 return e.gen, true
272}
273
274// BounceFor is Bounce for a scheduled attempt: the event names the attempt's
275// cause rather than "carlos restart" (spec §9.4).
276func (a *Service) BounceFor(host string, cause Cause) bool {
277 a.mu.Lock()
278 defer a.mu.Unlock()
279 e := a.entries[host]
280 if e == nil || e.state != actAwake {
281 return false
282 }
283 emitEvent(a.Logs, e.route, "info", bounceText(e.worker, cause))
284 a.beginStopLocked(e, stopWhyFor(cause))
285 return true
286}
287
288// stopWhyFor is the run-interruption reason (run spec §5.3) for a scheduled
289// attempt's bounce: a new build for release, install and retry, a restart
290// otherwise.
291func stopWhyFor(cause Cause) string {
292 switch cause {
293 case CauseRelease, CauseInstall, CauseRetry:
294 return StopRelease
295 }
296 return StopRestart
297}
298
299// bounceText is the one place BounceFor words its event. The words are
300// internal/rolloutwords, approved copy. The flag-off literal in
301// bounceLocked is never edited; restart's cause phrase keeps the two
302// identical.
303func bounceText(worker bool, cause Cause) string {
304 format := rolloutwords.EventBounceInstance
305 if worker {
306 format = rolloutwords.EventBounceSidecar
307 }
308 return fmt.Sprintf(format, rolloutwords.Cause(string(cause)))
309}
310
311// killFailed ends a failed first-wake build outright (spec §8.2 step 1),
312// bounded by firstWakeKillBound, and reports whether its end is proven. An
313// instance generation is killed with cgroup.kill on its own child and waited
314// empty (genKiller). Any error, errNoGenContainment included, is unproven:
315// without a generation child there is no kill that reaches what B forked,
316// and the good build must never start beside it. A worker is not a
317// generation child (plan 2 D4): its proven stop, bounded the same, stands in
318// for the kill, and THE termination proof (genTerminated on a Worker
319// generation) empties and checks its host cgroup. Never called with a.mu
320// held.
321func (a *Service) killFailed(rt registry.Route, g Gen, p Proc, worker bool) error {
322 ctx, cancel := context.WithTimeout(context.Background(), firstWakeKillBound)
323 defer cancel()
324 if !worker {
325 k, ok := a.backend.(genKiller)
326 if !ok {
327 return fmt.Errorf("%s: this backend cannot kill a generation", rt.Host)
328 }
329 if err := k.KillGen(ctx, rt, g.N); err != nil {
330 return err
331 }
332 // The kill proved the end. The handle is still released through
333 // its Stop, the only close a flat child's control listener gets
334 // (backend_exec.go): left bound, it is the stale-socket trap
335 // (#129). Its own bound, since the kill may have spent this one.
336 sctx, scancel := context.WithTimeout(context.Background(), firstWakeKillBound)
337 defer scancel()
338 _ = p.Stop(sctx)
339 return nil
340 }
341 if err := p.Stop(ctx); err != nil {
342 return err
343 }
344 if !a.genTerminated(rt, Gen{N: g.N, Build: g.Build, Worker: true}, p) {
345 return ErrStillRunning
346 }
347 return nil
348}
349
350// spawnPlanned is a planned wake's instance spawn (spec §8.2). The work
351// before the exec (lease, restore, seed) has already run once and is shared.
352//
353// Every exec, B's and its fallback's, goes through startPinnedAt with
354// planGate (PF7): the lease, the ownership and the plan's selection gate,
355// asked at the exec. A refusal there runs nothing and is abandonment.
356//
357// Execution evidence comes from the spawn's result, as startPinnedChecked
358// reads it: a returned process, or an unproven-start sentinel. Without it
359// nothing of the build ran and nothing is recorded. With it, only what
360// classifyStep calls the build's failure is recorded (a cancelled wake, a
361// stale attempt and a mismatched exec are not), and only for a first wake.
362//
363// For a first wake in overlap mode with a good build, B's health window
364// ends at the wake's start plus 55 s minus 25 s, a failed B is killed
365// outright, and the good build gets whatever time remains. When B's end is
366// not proven, nothing more starts. When less than firstWakeMinVerdict of
367// B's window is left at its exec (a slow lease, restore or seeding), B is
368// not exec'd at all: the good build boots instead and nothing is recorded,
369// since a health check cut that short would fail B for the clock's sake.
370func (a *Service) spawnPlanned(ctx context.Context, e *actEntry, rt registry.Route, plan WakePlan, w *wokenInstance, lease Lease) (Proc, string, Gen, error) {
371 plan, err := a.checkPlan(ctx, e, rt, plan)
372 if err != nil {
373 return nil, "", Gen{}, err
374 }
375 canFall := plan.FirstWake && plan.Fallback != nil && plan.Mode == ModeOverlap
376 boot := plan.Boot
377 gate := a.planGate(ctx, e, lease, plan, boot.Version)
378 var healthBy time.Time
379 var left time.Duration
380 if canFall {
381 healthBy = e.wakeStart.Add(firstWakeBudget - firstWakeKillReserve)
382 gate = verdictFloorGate(gate, healthBy, &left)
383 }
384 // Every exec here that runs the good build in another build's place is
385 // recorded with the planner's selection (ruling R-PLANNED-RECOVERY): a
386 // recovery boot (normally recorded at step 2b already), the good build
387 // booted untried and the fallback. Each exec's own context only.
388 proc, ver, g, err := a.startPinnedAt(withRecoverySelection(ctx, plan, rt, boot.Version), rt, genOf(boot), gate, healthBy)
389 w.bootRan = err == nil || startMayHaveRun(proc, err)
390 if proc == nil && errors.Is(err, errNoTimeForVerdict) {
391 // B never ran: too little of its window was left for a verdict at
392 // the exec. The good build boots in its place through the same
393 // gate, and the wake is reported as the good build's ordinary wake
394 // (w.untried): no failure of B, no fallback, no observation.
395 // Operator log only; nothing happened to the member's build.
396 good := *plan.Fallback
397 log.Printf("activator: %s: first wake of %s had %v of its health window left at the exec, under the %v floor; booting the good build %s without trying it",
398 rt.Host, boot.Version, left.Round(time.Millisecond), firstWakeMinVerdict, good.Version)
399 w.untried = true
400 gctx := WithPlannedSelection(ctx, PlannedSelection{Build: good.Version, InPlaceOf: boot.Version})
401 gproc, gver, gg, gerr := a.startPinnedAt(gctx, rt, genOf(good), a.planGate(ctx, e, lease, plan, good.Version), time.Time{})
402 if gerr != nil {
403 return gproc, gver, gg, fmt.Errorf("the good build %s, booted without trying %s: %w", good.Version, boot.Version, gerr)
404 }
405 return gproc, gver, withIdentity(gg, good), nil
406 }
407 if err == nil {
408 return proc, ver, withIdentity(g, boot), nil
409 }
410 // A nested start root accepted whose outcome is uncertain, with no
411 // process back, may have run B (Astra R3 ruling 206, zdd-readiness
412 // Task 6). In hold mode that is always B's failure: nothing falls back
413 // from a hold, and a wake reported as cancelled would leave the build
414 // free to boot again. In overlap mode only a unit that exited before it
415 // was observed (errStartedThenExited) is a proven crash at the start and
416 // fails B; any other uncertain overlap start stays "may have run"
417 // (afterWake's GenCancelled with Executed), unless its kill is
418 // unproven, which fails B below as before.
419 uncertain := proc == nil && startMayHaveRun(nil, err)
420 crashed := uncertain && errors.Is(err, errStartedThenExited)
421 uncertainFailure := uncertain && (plan.Mode == ModeHold || crashed)
422 executed := proc != nil || genStartUnproven(err)
423 if !uncertainFailure && (!executed || classifyStep(ctx, err) != stepFailed || errors.Is(err, errPinMismatch)) {
424 // Nothing of the build ran, or the wake's lifecycle ended it
425 // (maintenance, Shutdown, a stop), or its gate refused, or the
426 // backend ran something else: no failure of the build (spec §6.4,
427 // §7.4). Nothing is recorded and nothing falls back; doWake's
428 // cleanup stops a returned process and proves it gone.
429 return proc, ver, g, err
430 }
431 if plan.FirstWake {
432 w.failed, w.failReason = &boot, err.Error()
433 }
434 if !canFall {
435 return proc, ver, g, err
436 }
437 if proc == nil {
438 // No handle to kill. The good build starts only beside a B whose
439 // end is proven (plan review finding 9): a crash at the start that
440 // the backend's own kill proved gone (no errGenUnproven in the
441 // chain). Every other overlap start that reaches here with no
442 // process carries errGenUnproven: its end is unproven, so nothing
443 // more starts, the wake serves nothing, and doWake keeps the lease
444 // and the generation as an unproven wake.
445 if !crashed || genStartUnproven(err) {
446 return proc, ver, g, err
447 }
448 } else if kerr := a.killFailed(rt, g, proc, false); kerr != nil {
449 return proc, ver, g, fmt.Errorf("%w; its end was not proven, so the good build was not started: %v", err, kerr)
450 }
451 good := *plan.Fallback
452 fctx := WithPlannedSelection(ctx, PlannedSelection{Build: good.Version, InPlaceOf: boot.Version})
453 fproc, fver, fg, ferr := a.startPinnedAt(fctx, rt, genOf(good), a.planGate(ctx, e, lease, plan, good.Version), time.Time{})
454 if ferr != nil {
455 if fproc == nil && !genStartUnproven(ferr) {
456 // Nothing of the good build ran (its gate refused: a fence, a
457 // shutdown, a lost lease or entry since B's exec). B's failure is
458 // still recorded (w.failed); its process, proven gone, is what
459 // doWake's cleanup proves once more.
460 return proc, ver, g, fmt.Errorf("%w; the good build was not started: %v", err, ferr)
461 }
462 return fproc, fver, fg, fmt.Errorf("%w; the good build %s failed too: %w", err, good.Version, ferr)
463 }
464 w.fellBack = true
465 return fproc, fver, withIdentity(fg, good), nil
466}
467
468// errNoTimeForVerdict: at B's exec, less than firstWakeMinVerdict of its
469// health window was left. B is not exec'd; it is never a failure of B.
470var errNoTimeForVerdict = errors.New("too little of the first wake's health window is left for a verdict")
471
472// verdictFloorGate is gate with the first-wake floor asked after it, at
473// the exec: B's window is the earlier of exec + spawnHealthTimeout and
474// healthBy, less now. Below firstWakeMinVerdict it refuses with
475// errNoTimeForVerdict and stores the window in *left for the log. The
476// ownership, lease and plan checks go first, so their refusal (abandonment)
477// is never mistaken for a lack of time.
478func verdictFloorGate(gate func() error, healthBy time.Time, left *time.Duration) func() error {
479 return func() error {
480 if err := gate(); err != nil {
481 return err
482 }
483 now := time.Now()
484 end := now.Add(spawnHealthTimeout)
485 if healthBy.Before(end) {
486 end = healthBy
487 }
488 if *left = end.Sub(now); *left < firstWakeMinVerdict {
489 return errNoTimeForVerdict
490 }
491 return nil
492 }
493}
494
495// spawnPlannedWorker is spawnPlanned for a socketless child (spec §6.5).
496// Each spawn uses startWorker's contract exactly as doWake's worker branch
497// does (PF8): the build is published before its exec, planGate is asked at
498// the exec (after the publication), and on success the captured identity is
499// installed and the pending publication dropped in one critical section,
500// then published again. A backend without the staged capability cannot
501// exec a named build, so a planned worker is refused there before anything
502// runs (errSwapNoGens). plannedWorkerReady stands in for the health check,
503// and a failed release-cause first wake in overlap mode is replaced by the
504// good build once the failed child's host cgroup is proven empty.
505func (a *Service) spawnPlannedWorker(ctx context.Context, e *actEntry, rt registry.Route, plan WakePlan, env map[string]string, w *wokenInstance, lease Lease) (Proc, release.StagedIdentity, error) {
506 if _, ok := a.backend.(stagedWorkerStarter); !ok {
507 return nil, release.StagedIdentity{}, errSwapNoGens
508 }
509 plan, err := a.checkPlan(ctx, e, rt, plan)
510 if err != nil {
511 return nil, release.StagedIdentity{}, err
512 }
513 start := func(sctx context.Context, id release.StagedIdentity) (Proc, release.StagedIdentity, error) {
514 p, got, pend, err := a.startWorker(sctx, rt, env, &id, a.planGate(ctx, e, lease, plan, id.Version))
515 if err != nil {
516 return p, got, err
517 }
518 a.mu.Lock()
519 e.workerServing = got
520 a.dropPendingGenerationLocked(pend)
521 a.mu.Unlock()
522 _ = a.persistGenerations()
523 if got.StagedPath != id.StagedPath {
524 return p, got, fmt.Errorf("%w: worker build %s from %q; the plan chose %s from %q", errPinMismatch, got.Version, got.StagedPath, id.Version, id.StagedPath)
525 }
526 return p, got, plannedWorkerReady(ctx, p)
527 }
528 canFall := plan.FirstWake && plan.Fallback != nil && plan.Mode == ModeOverlap
529 boot := plan.Boot
530 proc, id, err := start(withRecoverySelection(ctx, plan, rt, boot.Version), boot)
531 w.bootRan = err == nil || startMayHaveRun(proc, err)
532 if err == nil || proc == nil {
533 // Ready, or nothing of the build ran: a refusal at the gate, an
534 // unpublished build (ErrBuildNotPublished), a spawn error with no
535 // child.
536 return proc, id, err
537 }
538 if classifyStep(ctx, err) != stepFailed || errors.Is(err, errPinMismatch) {
539 // A cancelled readiness window is the wake's lifecycle, not the
540 // build (spec §6.4, §6.5): nothing recorded, nothing falls back.
541 return proc, id, err
542 }
543 if plan.FirstWake {
544 w.failed, w.failReason = &boot, err.Error()
545 }
546 if !canFall {
547 return proc, id, err
548 }
549 if kerr := a.killFailed(rt, Gen{Build: id.Version}, proc, true); kerr != nil {
550 return proc, id, fmt.Errorf("%w; its end was not proven, so the good build was not started: %v", err, kerr)
551 }
552 // The fallback is recorded with the planner's selection (ruling
553 // R-PLANNED-RECOVERY), as spawnPlanned's is.
554 fproc, fid, ferr := start(WithPlannedSelection(ctx, PlannedSelection{Build: plan.Fallback.Version, InPlaceOf: boot.Version}), *plan.Fallback)
555 if ferr != nil {
556 if fproc == nil {
557 // Nothing of the good build ran: B's failure is still recorded,
558 // and B's child, proven gone, is what doWake's cleanup proves.
559 return proc, id, fmt.Errorf("%w; the good build was not started: %v", err, ferr)
560 }
561 return fproc, fid, fmt.Errorf("%w; the good build %s failed too: %w", err, plan.Fallback.Version, ferr)
562 }
563 w.fellBack = true
564 return fproc, fid, nil
565}
566
567// plannedWorkerReady is spec §6.5's readiness for a socketless planned
568// wake, the rule ReplaceWorker applies too: the child is ready once it is
569// still running firstWakeSettle (10 s) after it started, and a clean exit
570// inside that window is a one-shot's success. Any other exit fails. An early
571// ready report does not shorten the window. The flag-off wake keeps
572// awaitWorkerSettle (ready report, sidecarSettle, every early exit a
573// failure), unchanged.
574func plannedWorkerReady(ctx context.Context, p Proc) error {
575 switch observeSettle(ctx, p, firstWakeSettle) {
576 case settleOK:
577 return nil
578 case settleExited:
579 if !procCrashed(p) {
580 return nil
581 }
582 return errWorkerExitedEarly
583 }
584 if err := ctx.Err(); err != nil {
585 return err
586 }
587 return context.Canceled
588}
589
590// outcomeFence counts a host's planned-wake outcomes that have been taken
591// but not yet delivered to OnGen (final review F3). done closes when the
592// count returns to zero.
593type outcomeFence struct {
594 n int
595 done chan struct{}
596}
597
598// holdOutcomeLocked takes one of host's outcome fences and returns its
599// release. The release takes a.mu itself, so it is called off a.mu, after
600// the outcome's OnGen has returned; only its first call counts. The caller
601// holds a.mu.
602func (a *Service) holdOutcomeLocked(host string) func() {
603 if a.outcomeFences == nil {
604 a.outcomeFences = make(map[string]*outcomeFence)
605 }
606 f := a.outcomeFences[host]
607 if f == nil {
608 f = &outcomeFence{done: make(chan struct{})}
609 a.outcomeFences[host] = f
610 }
611 f.n++
612 var once sync.Once
613 return func() {
614 once.Do(func() {
615 a.mu.Lock()
616 defer a.mu.Unlock()
617 if f.n--; f.n == 0 {
618 close(f.done)
619 if a.outcomeFences[host] == f {
620 delete(a.outcomeFences, host)
621 }
622 }
623 })
624 }
625}
626
627// errOutcomeUndelivered refuses a wake, before it plans, while an earlier
628// planned wake's outcome is still being delivered past outcomeFenceWait
629// (final review wave 2, N1). The fence fails closed: planning without the
630// outcome could boot a build whose failure (a hold's included) is not yet
631// recorded. Members get the maintenance fence's existing text
632// (memberSafeWakeReason): the platform holds the host closed until its own
633// work is done, and it reopens by itself.
634var errOutcomeUndelivered = errors.New("an earlier planned wake's outcome is still being delivered")
635
636// awaitOutcomes is a new wake's wait, before it plans, for host's
637// planned-wake outcomes still being delivered (final review F3; spec §8.1,
638// O7): a failed first wake's entry goes before afterWake's OnGen records
639// the failure, and a first wake's observer reaches its verdict before
640// OnGen hears it. Planning in that gap would pick the failed build again,
641// with no record yet to hold it. The wait is bounded by outcomeFenceWait
642// and the wake's own context; the fence is not (final review wave 2, N1).
643// A wait that ends before the delivery refuses the wake: an error means
644// the caller must not plan or launch anything. The fence stays until the
645// outcome's OnGen returns, so the next request waits again. Called off
646// a.mu.
647func (a *Service) awaitOutcomes(ctx context.Context, host string) error {
648 a.mu.Lock()
649 f := a.outcomeFences[host]
650 a.mu.Unlock()
651 if f == nil {
652 return nil
653 }
654 bound := time.NewTimer(outcomeFenceWait)
655 defer bound.Stop()
656 select {
657 case <-f.done:
658 return nil
659 case <-ctx.Done():
660 // Refused the same way: the outcome is still undelivered.
661 return fmt.Errorf("%w: %w", errOutcomeUndelivered, ctx.Err())
662 case <-bound.C:
663 log.Printf("activator: %s: an earlier planned wake's outcome is still being delivered after %v; refusing this wake until it is", host, outcomeFenceWait)
664 return errOutcomeUndelivered
665 }
666}
667
668// outcomeOwedLocked reports whether wake w of e ends with a failure that
669// afterWake will deliver (GenFailed, GenFellBack): the outcome fence covers
670// it. The caller holds a.mu.
671func (a *Service) outcomeOwedLocked(e *actEntry, w *wokenInstance, err error) bool {
672 return a.OnGen != nil && w != nil && e.plan.planned() && (w.failed != nil || (err != nil && w.holdRanUnjudged(e.plan)))
673}
674
675// holdRanUnjudged reports whether a failed wake of plan ran (or may have
676// run) its first wake's hold build without a verdict on it (final review
677// wave 2, N2): its lifecycle ended it after the exec (a stop, a fence, a
678// step after the spawn), or its start's outcome is uncertain. afterWake
679// reports it as GenCancelled with Executed, and the outcome fence covers
680// the delivery, so the next wake plans only once the scheduler holds the
681// instance off its good build.
682func (w *wokenInstance) holdRanUnjudged(plan WakePlan) bool {
683 return w.bootRan && w.failed == nil && plan.FirstWake && plan.Mode == ModeHold
684}
685
686// emitPlanned is emitGen for an event of a wake that ran plan: it carries
687// the plan's token and config revision (final review F4), so every event of
688// one planned wake, on every path, names the same wake. Never called with
689// a.mu held (emitGen's rule).
690func (a *Service) emitPlanned(plan WakePlan, ev GenEvent) {
691 ev.Token, ev.ConfigRev = plan.Token, plan.ConfigRev
692 a.emitGen(ev)
693}
694
695// emitGen hands ev to OnGen. It is never called with a.mu held: OnGen
696// reaches the scheduler's served, publishServing and writeGood, which take
697// its goodMu and call back into Current.
698func (a *Service) emitGen(ev GenEvent) {
699 if a.OnGen != nil {
700 a.OnGen(ev)
701 }
702}
703
704// cancelPlanned reports that a planned wake of plan ended with no verdict
705// on its build and nothing owed to the outcome fence (rollout Task 5, plan
706// review #1): refused at its lease, its exec or its check, replaced by a
707// replan, or (ran true) an overlap or ordinary wake its lifecycle ended
708// after the exec. It records nothing; it is the wake's terminal event, which
709// ends the scheduler's write-ahead intent for it. Only a plan with a token
710// is reported, and only to an OnGen that is set, so a box with the flag off
711// hears nothing. Never called with a.mu held (emitGen's rule).
712func (a *Service) cancelPlanned(rt registry.Route, plan WakePlan, ran bool) {
713 if a.OnGen == nil || plan.Token == 0 || !plan.planned() {
714 return
715 }
716 ev := GenEvent{Kind: GenCancelled, Route: rt, Mode: plan.Mode, FirstWake: plan.FirstWake, At: time.Now(), Executed: ran}
717 if ran {
718 ev.Gen = genOf(plan.Boot)
719 }
720 a.emitPlanned(plan, ev)
721}
722
723// afterWake reports a planned wake's outcome. wake() calls it after
724// e.ready has closed, without a.mu (emitGen's rule).
725//
726// Every planned wake with a token ends with exactly one terminal event for
727// it (rollout Task 5): its failure, its fallback, its verdict or its
728// cancellation here, or the first-wake observer's. A failed wake that owed
729// nothing to the outcome fence (outcomeOwedLocked) reports its cancellation
730// (cancelPlanned) after its entry is gone and admission has reopened; that
731// is safe because the event records nothing, and a newer wake's intent
732// replaces this one's, so its late arrival ends nothing it should not.
733func (a *Service) afterWake(e *actEntry, w *wokenInstance, err error) {
734 plan := e.plan // written only by this wake's own goroutine
735 if a.OnGen == nil || !plan.planned() {
736 return
737 }
738 if w == nil {
739 // No result at all (a worker's token not minted, a claimed wake that
740 // ran nothing of the planned build): nothing of it ran.
741 if err != nil {
742 a.cancelPlanned(e.route, plan, false)
743 }
744 return
745 }
746 now := time.Now()
747 switch {
748 case err != nil:
749 switch {
750 case w.failed != nil:
751 a.emitPlanned(plan, GenEvent{Kind: GenFailed, Route: e.route, Failed: *w.failed, Mode: plan.Mode, FirstWake: true, Reason: w.failReason, At: now, FailedUnder: plan.FailedUnder})
752 case w.holdRanUnjudged(plan):
753 a.emitPlanned(plan, GenEvent{Kind: GenCancelled, Route: e.route, Gen: genOf(plan.Boot), Mode: plan.Mode, FirstWake: true, At: now, Executed: true})
754 default:
755 // Refused before the exec (its lease, a fence, its check), or an
756 // overlap or ordinary wake ended after the exec with no verdict.
757 a.cancelPlanned(e.route, plan, w.bootRan)
758 }
759 case w.fellBack:
760 a.emitPlanned(plan, GenEvent{Kind: GenFellBack, Route: e.route, Gen: w.planned, Failed: *w.failed, Mode: plan.Mode, FirstWake: true, Reason: w.failReason, At: now, FailedUnder: plan.FailedUnder})
761 case w.untried:
762 // B was never exec'd (too little time for a verdict) and the good
763 // build serves: reported exactly as a healthy wake of the good build
764 // that is not a first wake, so the scheduler records it as served
765 // and B stays a candidate for the next wake.
766 a.emitPlanned(plan, GenEvent{Kind: GenHealthy, Route: e.route, Gen: w.planned, Mode: plan.Mode, At: now})
767 default:
768 a.emitPlanned(plan, GenEvent{Kind: GenHealthy, Route: e.route, Gen: w.planned, Mode: plan.Mode, FirstWake: plan.FirstWake, At: now})
769 switch {
770 case plan.FirstWake && e.worker:
771 // A socketless first wake's readiness window (plannedWorkerReady,
772 // spec §6.5) was its settle: it is settled now. A second window
773 // would fail a build that was already ready, where §8.6 calls for
774 // ordinary crash recovery.
775 a.emitPlanned(plan, GenEvent{Kind: GenSettled, Route: e.route, Gen: w.planned, Mode: plan.Mode, FirstWake: true, At: now})
776 case plan.FirstWake:
777 a.watchFirstWake(e, w.planned, plan)
778 }
779 }
780}
781
782// armFirstWatchLocked registers a first wake's observation in the awake
783// transition, before e.ready closes (spec §6.4): Current hides the
784// generation from the instant it is reachable, and an exit or a stop
785// before the observer goroutine runs is classified by it, never lost.
786// wake() calls it after e.gen is set, and before the fence and bounce
787// checks there, so a stop they begin cancels the observation. It also takes
788// the host's outcome fence (final review F3), which watchFirstWake releases
789// once its terminal event has been delivered: the entry can be stopped and
790// a new wake begun before then, and that wake must not plan until the
791// verdict is recorded. The caller holds a.mu.
792func (a *Service) armFirstWatchLocked(e *actEntry, w *wokenInstance) {
793 if a.Planner == nil || a.OnGen == nil || e.worker || !e.plan.planned() || !e.plan.FirstWake || w.fellBack || w.failed != nil || w.untried {
794 return
795 }
796 ctx, cancel := context.WithCancel(context.Background())
797 e.firstWatch, e.firstWatchCtx = cancel, ctx
798 e.firstWatchRelease = a.holdOutcomeLocked(e.host)
799}
800
801// watchFirstWake is a first wake's 10 s observation (spec §6.4), on plan 3's
802// observeSettle. The waiting requests do not sit through it. An intentional
803// stop cancels it (beginStopLocked) and records nothing; an unexpected exit
804// fails the build.
805func (a *Service) watchFirstWake(e *actEntry, g Gen, plan WakePlan) {
806 cancelled := func(rt registry.Route) {
807 // Every observation GenHealthy opened is closed by an event; this
808 // one says "no verdict", so the scheduler stops showing the host
809 // as settling and records nothing (spec §6.4, §9.1). The build was
810 // healthy, so it ran (Executed).
811 a.emitPlanned(plan, GenEvent{Kind: GenCancelled, Route: rt, Gen: g, Mode: plan.Mode, FirstWake: true, At: time.Now(), Executed: true})
812 }
813 // The observation was registered in the awake transition
814 // (armFirstWatchLocked), so a stop that began meanwhile has already
815 // cancelled it, or, for a crash, marked unexpectedExit: observeSettle and
816 // the classification below decide either way. Nothing registered means
817 // no first-wake observation for this wake.
818 a.mu.Lock()
819 ctx, rt, proc := e.firstWatchCtx, e.route, e.proc
820 a.mu.Unlock()
821 if ctx == nil {
822 cancelled(rt)
823 return
824 }
825 go func() {
826 r := observeSettle(ctx, proc, firstWakeSettle)
827 a.mu.Lock()
828 rt, unexpected, release := e.route, e.unexpectedExit, e.firstWatchRelease
829 a.mu.Unlock()
830 // end closes the observation: Current shows the generation again,
831 // and a stop no longer has an observation to cancel.
832 end := func() {
833 a.mu.Lock()
834 cancel := e.firstWatch
835 e.firstWatch, e.firstWatchCtx, e.firstWatchRelease = nil, nil, nil
836 a.mu.Unlock()
837 if cancel != nil {
838 cancel()
839 }
840 }
841 // The verdict is delivered before the host's outcome fence is
842 // released (final review F3): a new wake that began meanwhile plans
843 // only once the scheduler has recorded it.
844 defer func() {
845 if release != nil {
846 release()
847 }
848 }()
849 exited := r == settleExited || (r == settleCancelled && unexpected)
850 switch {
851 case r == settleOK:
852 // Ended first: the scheduler records a settled build as good
853 // only while Current shows it (rollout's acknowledged).
854 end()
855 a.emitPlanned(plan, GenEvent{Kind: GenSettled, Route: rt, Gen: g, Mode: plan.Mode, FirstWake: true, At: time.Now()})
856 case exited:
857 // Delivered first, so nothing sees the failed generation as
858 // current (and admits an attempt onto it) before the failure is
859 // recorded.
860 a.emitPlanned(plan, GenEvent{Kind: GenFailed, Route: rt, Failed: plan.Boot, Mode: plan.Mode, FirstWake: true,
861 Reason: "exited within " + firstWakeSettle.String() + " of becoming healthy", At: time.Now(), FailedUnder: plan.FailedUnder})
862 end()
863 default: // an intentional stop cancelled it
864 cancelled(rt)
865 end()
866 }
867 }()
868}
869
870// claimedPlanResult carries a claims-on planned wake's outcome to afterWake.
871// Plan 3's startAdmitted records the pinned build it saw fail
872// (e.pinFailed) and, once the isolated launch opens, starts the good build
873// in its place (StartPolicy.Fallback). w is the claim's result, nil when
874// the wake failed. The returned value is what doClaimWake hands back to
875// wake(). On this base every claims-on launch refuses before its exec hook,
876// so pinFailed stays nil and a planned claimed wake reports nothing.
877func (a *Service) claimedPlanResult(e *actEntry, w *wokenInstance) *wokenInstance {
878 a.mu.Lock()
879 failed, why, plan := e.pinFailed, e.pinFailReason, e.plan
880 a.mu.Unlock()
881 if !plan.planned() {
882 return w
883 }
884 if w == nil {
885 if failed == nil || !plan.FirstWake {
886 return nil // nothing of the planned build ran: no failure of the build
887 }
888 return &wokenInstance{failed: &plan.Boot, failReason: why}
889 }
890 served := plan.Boot
891 if failed != nil && plan.FirstWake && plan.Fallback != nil && w.spawnVersion == plan.Fallback.Version {
892 served = *plan.Fallback
893 w.failed, w.failReason, w.fellBack = &plan.Boot, why, true
894 }
895 w.planned = genOf(served)
896 w.planned.N, w.planned.Socket, w.planned.Control = w.gen.N, w.gen.Socket, w.gen.Control
897 return w
898}
899