carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package activator
2
3import (
4 "context"
5 "errors"
6 "log"
7 "sync"
8 "time"
9
10 "github.com/carlosframework/platform/internal/instancectl"
11 "github.com/carlosframework/platform/internal/instancekey"
12 "github.com/carlosframework/platform/internal/registry"
13)
14
15// Why an instance stop interrupted a run (run spec §5.3). The text is what
16// the member reads after "the run was interrupted:" (RUN-C1 to RUN-C8);
17// beginStopLocked records one on every stop.
18const (
19 StopIdle = "the instance hibernated"
20 StopRestart = "the instance was restarted"
21 StopRelease = "the instance moved to a new release"
22 StopExited = "the instance's process exited"
23 StopLeaseLost = "the instance lost its database lease"
24 StopFenced = "the instance was stopped for a move or a layout change"
25 StopRetired = "the instance was taken off this box"
26 StopShutdown = "the box's agent stopped"
27)
28
29// ErrCmdClaim: claim control is armed on this box. Runs need the isolated
30// root protocol's reservations (#250), so they are refused (run spec §3.1).
31var ErrCmdClaim = errors.New("activator: claim control is armed; runs wait for the reservation protocol (#250)")
32
33// ErrCmdBusy: a run is already attached to the instance, or an earlier
34// run's unit was never proven settled.
35var ErrCmdBusy = errors.New("activator: a run is already attached to this instance")
36
37// ErrCmdNotInstance: runs are for instances; the route is a worker or
38// another kind.
39var ErrCmdNotInstance = errors.New("activator: runs are only for instances, and this route is not one")
40
41// CmdWakeError: the wake failed or did not finish within cmdWakeBound.
42// Reason is member-safe (memberSafeWakeReason).
43type CmdWakeError struct {
44 Reason string
45 Err error
46}
47
48func (e *CmdWakeError) Error() string { return e.Reason }
49func (e *CmdWakeError) Unwrap() error { return e.Err }
50
51// CmdStoppedError: the instance began stopping before the unit started. The
52// run is interrupted with Reason, never queued for later (run spec §5.3).
53type CmdStoppedError struct{ Reason string }
54
55func (e *CmdStoppedError) Error() string {
56 return "activator: the instance stopped before the run started: " + e.Reason
57}
58
59// cmdWakeBound is how long RunCmd waits for a wake (run spec §5.3).
60var cmdWakeBound = 60 * time.Second
61
62// cmdStopBound bounds finishStop's wait for a run: the start in flight
63// (cmdStartBound), the stop (DeadlineControl), the cgroup (actStopTimeout)
64// and both streams (cmdAcceptGrace + cmdDrainBound each), with room.
65// Shutdown runs inside systemd's 90 s TimeoutStopSec for the agent: a
66// settle (about 62 s) fits; a start in flight on top may not, and an agent
67// killed mid-stop leaves the lease to its TTL.
68const cmdStopBound = 2 * time.Minute
69
70// cmdStartBound bounds RunCmd's StartCmd, well below cmdStopBound, so a
71// stop waiting on the start still has the settle's budget after it.
72const cmdStartBound = 45 * time.Second
73
74// CmdHandle is a started run, as the box's run pass sees it.
75type CmdHandle interface {
76 Invocation() string
77 Poll(ctx context.Context) (instancectl.CmdStatus, error)
78 // Interrupted is the instance stop's reason when one stopped the unit,
79 // "" otherwise, and when that stop was asked for: a command whose done
80 // status the caller had before then finished on its own.
81 Interrupted() (reason string, at time.Time)
82 // Stop stops the unit and waits for it to settle and its streams to
83 // end, leaving the instance running: a cancel, a timeout, or the end
84 // of a command that exited. Safe to call more than once.
85 Stop(ctx context.Context) error
86 // Release clears the run from the entry once its result is written.
87 Release()
88}
89
90// cmdHold is the entry's cmd field.
91type cmdHold struct {
92 runID string
93 // starting is set under a.mu by RunCmd's last awake check: from then
94 // on a stop waits for the start. Before it, a stop does not wait: the
95 // check will see the entry stopping and abandon the run.
96 starting bool
97 // cancelPrepare cancels spec.Prepare's context; beginStopLocked calls
98 // it, under a.mu, so a stop interrupts a download.
99 cancelPrepare context.CancelFunc
100 started chan struct{} // closed once StartCmd returned, either way
101 unit CmdUnit // written before started is closed; nil when no unit can be running
102 now func() time.Time
103 mu sync.Mutex
104 asked bool // a stop has been asked for; under mu
105 reason string
106 askedAt time.Time // when the first stop was asked for; under mu
107 // settling is the settle attempt in flight, nil when none is. A stop
108 // joins it rather than starting a second one. Under mu.
109 settling *cmdSettle
110 // proven: a settle attempt succeeded, so nothing of the unit runs. It
111 // is never unset. A failed attempt leaves it false, and the next stop
112 // tries again (merge ruling M1): a failed settle is never cached as
113 // the run's last word while the instance's ownership is given up.
114 // Under mu.
115 proven bool
116 // lastTry is when the last settle attempt ended, by h.now; the sweep's
117 // retry keeps actStopTimeout after it (retrySettle). Under mu.
118 lastTry time.Time
119 // gen is the generation the run was admitted against (merge ruling
120 // M2): its number and its exe pin. RunCmd sets it under a.mu with
121 // starting, and it is never written again. The command execs gen's
122 // build; a swap or a scheduled restart that retires gen interrupts
123 // the run (interruptCmdsLocked), and the generations file names gen's
124 // build until the run is proven settled (entryGenerationsLocked).
125 gen Gen
126 // onProven, when set, runs once a settle has proven the unit gone,
127 // with no lock held: RunCmd's republication of the generations file,
128 // whose pin for gen may have been this run's alone. Set before the
129 // unit starts and never written again.
130 onProven func()
131}
132
133// cmdSettle is one settle attempt: done closes when it has ended, and err
134// is its result, written before done closes.
135type cmdSettle struct {
136 done chan struct{}
137 err error
138}
139
140func newCmdHold(runID string, cancelPrepare context.CancelFunc, now func() time.Time) *cmdHold {
141 return &cmdHold{runID: runID, cancelPrepare: cancelPrepare, now: now, started: make(chan struct{})}
142}
143
144// stop waits for the start to return, records reason and the time if it is
145// the first stop asked for (the run's own Stop passes "", so an instance
146// stop after it is not what ended the run), and stops and settles the unit.
147// A unit already proven settled returns nil at once. Otherwise stop joins
148// the attempt in flight, or starts one, and returns that attempt's result:
149// an attempt that failed is tried again by the next stop.
150func (h *cmdHold) stop(ctx context.Context, reason string) error {
151 select {
152 case <-h.started:
153 case <-ctx.Done():
154 return ctx.Err()
155 }
156 h.mu.Lock()
157 if !h.asked {
158 h.asked = true
159 h.reason, h.askedAt = reason, h.now()
160 }
161 if h.unit == nil || h.proven {
162 h.mu.Unlock()
163 return nil
164 }
165 at := h.settling
166 if at == nil {
167 at = &cmdSettle{done: make(chan struct{})}
168 h.settling = at
169 go h.settle(at)
170 }
171 h.mu.Unlock()
172 select {
173 case <-at.done:
174 return at.err
175 case <-ctx.Done():
176 return ctx.Err()
177 }
178}
179
180// settle is one attempt: the unit's StopAndSettle, unbounded by any
181// caller's context (StopAndSettle bounds itself), so a caller that gives
182// up waiting never cuts a settle short.
183func (h *cmdHold) settle(at *cmdSettle) {
184 err := h.unit.StopAndSettle(context.Background())
185 h.mu.Lock()
186 at.err = err
187 if err == nil {
188 h.proven = true
189 }
190 h.settling, h.lastTry = nil, h.now()
191 h.mu.Unlock()
192 close(at.done)
193 if err == nil && h.onProven != nil {
194 h.onProven()
195 }
196}
197
198// retrySettle starts one settle attempt in the background, for the sweep
199// (re-review 6), when the unit has started and is not proven settled, no
200// attempt is in flight, and the last one ended at least every ago. A hold
201// still attached to its run is retried only once a stop has been asked
202// for it (an interruption, or the run's own Stop that failed): a running
203// command nobody asked to stop is left alone. A released hold is retried
204// whatever: its run is over, and only its proof is missing. The reason a
205// stop recorded is kept. It never blocks.
206func (h *cmdHold) retrySettle(released bool, every time.Duration) {
207 select {
208 case <-h.started:
209 default:
210 return
211 }
212 h.mu.Lock()
213 defer h.mu.Unlock()
214 if h.unit == nil || h.proven || h.settling != nil || (!released && !h.asked) || h.now().Sub(h.lastTry) < every {
215 return
216 }
217 if !h.asked {
218 h.asked, h.askedAt = true, h.now()
219 }
220 at := &cmdSettle{done: make(chan struct{})}
221 h.settling = at
222 go h.settle(at)
223}
224
225// provenSettled reports whether the hold leaves nothing running: the start
226// returned no unit, or a settle of the unit succeeded. A failed start that
227// reached the control service returns a unit, since the unit may be
228// running.
229func (h *cmdHold) provenSettled() bool {
230 select {
231 case <-h.started:
232 default:
233 return false
234 }
235 if h.unit == nil {
236 return true
237 }
238 h.mu.Lock()
239 defer h.mu.Unlock()
240 return h.proven
241}
242
243type cmdRun struct {
244 a *Service
245 e *actEntry
246 h *cmdHold
247 once sync.Once
248}
249
250func (r *cmdRun) Invocation() string { return r.h.unit.Invocation() }
251func (r *cmdRun) Poll(ctx context.Context) (instancectl.CmdStatus, error) {
252 return r.h.unit.Poll(ctx)
253}
254func (r *cmdRun) Interrupted() (string, time.Time) {
255 r.h.mu.Lock()
256 defer r.h.mu.Unlock()
257 return r.h.reason, r.h.askedAt
258}
259func (r *cmdRun) Stop(ctx context.Context) error { return r.h.stop(ctx, "") }
260
261// Release clears the hold and stamps activity, so the instance idles a
262// full window after a run rather than hibernating at the next sweep. A
263// unit not proven settled (its Stop failed or never came) stays the
264// instance's: the next stop settles it before replication closes, and
265// keeps replication and the lease until it has (run spec §5.3, codex 1;
266// merge ruling M1).
267func (r *cmdRun) Release() {
268 r.once.Do(func() {
269 r.a.mu.Lock()
270 if r.e.cmd == r.h {
271 r.e.cmd = nil
272 r.e.lastActive = r.a.now()
273 if !r.h.provenSettled() {
274 r.e.cmdLeft = append(r.e.cmdLeft, r.h)
275 }
276 }
277 r.a.mu.Unlock()
278 })
279}
280
281// cmdBusyLocked reports whether e cannot take a run: one is attached, or
282// an earlier one's unit was never proven settled. It drops the released
283// holds that have since settled. The caller holds a.mu.
284func cmdBusyLocked(e *actEntry) bool {
285 left := e.cmdLeft[:0]
286 for _, h := range e.cmdLeft {
287 if !h.provenSettled() {
288 left = append(left, h)
289 }
290 }
291 clear(e.cmdLeft[len(left):])
292 e.cmdLeft = left
293 return e.cmd != nil || len(left) > 0
294}
295
296// cmdHoldsLocked is every run of e whose unit may still be running: the
297// released runs not proven settled, and the attached run once it is
298// starting. An attached run not yet starting is not one: RunCmd's last
299// awake check sees a stop and abandons it before any unit starts. The
300// caller holds a.mu.
301func cmdHoldsLocked(e *actEntry) []*cmdHold {
302 var holds []*cmdHold
303 for _, h := range e.cmdLeft {
304 if !h.provenSettled() {
305 holds = append(holds, h)
306 }
307 }
308 if e.cmd != nil && e.cmd.starting && !e.cmd.provenSettled() {
309 holds = append(holds, e.cmd)
310 }
311 return holds
312}
313
314// cmdStartInFlightLocked reports whether e's run has passed RunCmd's last
315// check and its StartCmd has not returned: a unit may be starting against
316// e's current generation right now. An overlap swap is not admitted, and
317// does not switch, while one is (merge ruling M2). The caller holds a.mu.
318func cmdStartInFlightLocked(e *actEntry) bool {
319 if e.cmd == nil || !e.cmd.starting {
320 return false
321 }
322 select {
323 case <-e.cmd.started:
324 return false
325 default:
326 return true
327 }
328}
329
330// switchPendingLocked is a live overlap successor of e: a member spawning
331// or health-checking whose owner has not given up on it (an unproven
332// member's attempt is over). While there is one, e's current generation
333// may retire at any moment, so RunCmd starts no unit against it, and waits
334// for the switch or the successor's end (merge ruling M2). nil when there
335// is none. The caller holds a.mu.
336func switchPendingLocked(e *actEntry) *genMember {
337 for _, m := range e.ops.members {
338 if (m.role == roleSpawning || m.role == roleSuccessor) && !m.unproven {
339 return m
340 }
341 }
342 return nil
343}
344
345// cmdOwnershipLocked is why e must not start a run now, or "": the entry
346// is the host's and awake with its lease (ownedLocked), and its current
347// generation's process is running. Each refusal is the stop reason it
348// will become (re-review 5). The caller holds a.mu.
349func cmdOwnershipLocked(a *Service, e *actEntry) string {
350 switch err := a.ownedLocked(e, false); {
351 case errors.Is(err, errSwapLeaseLost):
352 return StopLeaseLost
353 case errors.Is(err, errSwapFenced):
354 return StopFenced
355 case errors.Is(err, errSwapShutdown):
356 return StopShutdown
357 case err != nil:
358 return StopIdle
359 }
360 if e.proc == nil || e.proc.Exited() {
361 return StopExited
362 }
363 return ""
364}
365
366// cmdSwitchPoll is how often RunCmd re-checks a pending switch it waits on,
367// besides the successor's own done.
368var cmdSwitchPoll = 100 * time.Millisecond
369
370// interruptCmdsLocked interrupts every run of e admitted against
371// generation n, which is retiring (merge ruling M2): each unit is stopped
372// and settled off a.mu, and the run learns why, which the member reads as
373// interrupted (exit 125, RUN-C75). A settle that fails leaves the run
374// unproven: its own Stop or the instance's next stop tries again, and the
375// generations file keeps naming n's build until one succeeds. The caller
376// holds a.mu.
377func (a *Service) interruptCmdsLocked(e *actEntry, n uint64, why string) {
378 for _, h := range cmdHoldsLocked(e) {
379 if h.gen.N != n {
380 continue
381 }
382 go func(h *cmdHold) {
383 ctx, cancel := context.WithTimeout(context.Background(), cmdStopBound)
384 defer cancel()
385 if err := h.stop(ctx, why); err != nil {
386 log.Printf("activator: %s: run %s on retired generation %d is not proven stopped: %v; the sweep tries again", e.host, h.runID, n, err)
387 }
388 }(h)
389 }
390}
391
392// retryCmdSettlesLocked is the sweep's retry of every run of e whose stop
393// was asked for and is not proven (re-review 6): a run interrupted when a
394// swap retired its generation, a run whose own Stop failed, and a released
395// run left unsettled. It runs whatever state e is in, pending stop or
396// awake, so an instance that stays awake still gets its runs proven, their
397// pins dropped and later runs admitted. Each attempt runs off a.mu, one at
398// a time per run, actStopTimeout apart. The caller holds a.mu.
399func (a *Service) retryCmdSettlesLocked(e *actEntry) {
400 for _, h := range e.cmdLeft {
401 h.retrySettle(true, actStopTimeout)
402 }
403 if e.cmd != nil && e.cmd.starting {
404 e.cmd.retrySettle(false, actStopTimeout)
405 }
406}
407
408// settleCmds is a run's part of an instance's termination proof (run spec
409// §5.3; merge ruling M1): every run of e whose unit may still be running is
410// stopped and settled, bounded by cmdStopBound, and it reports whether none
411// may be running now. Proven means the unit stopped and its cgroup is empty
412// (cmdUnit.StopAndSettle). A settle that fails is not given up on: the stop
413// stays pending with replication and the lease held, and the sweep's next
414// pass calls this again (finishPendingStops). Never call it with a.mu held.
415func (a *Service) settleCmds(e *actEntry) bool {
416 a.mu.Lock()
417 holds, why := cmdHoldsLocked(e), e.stopWhy
418 a.mu.Unlock()
419 if len(holds) == 0 {
420 return true
421 }
422 ctx, cancel := context.WithTimeout(context.Background(), cmdStopBound)
423 defer cancel()
424 proven := true
425 for _, h := range holds {
426 if err := h.stop(ctx, why); err != nil {
427 log.Printf("activator: %s: run %s is not proven stopped: %v", e.host, h.runID, err)
428 proven = false
429 }
430 }
431 return proven
432}
433
434// RunCmd runs one command against rt's instance (run spec §5.2 and §5.3):
435// refused with claim control armed, for anything but an instance, or on a
436// layout other than nested; the instance woken through the ordinary wake
437// path, waiting up to 60 s; the run attached to the awake entry;
438// spec.Prepare run under the hold, its context cancelled by a stop; and
439// the unit started only while the entry is still awake. A stop that begins
440// before the last awake check does not wait for the run, which then ends
441// here (CmdStoppedError, no unit). One that begins after it waits in
442// finishStop for the start to return and then stops the unit before
443// replication closes.
444//
445// ctx is the run's own: its deadline and cancellation span the whole
446// admission, the wake, Prepare, any wait for a swap's switch and the last
447// checks (re-review 5). A run whose ctx ends before the start returns
448// ctx's error and starts nothing.
449func (a *Service) RunCmd(ctx context.Context, rt registry.Route, spec CmdSpec) (CmdHandle, error) {
450 if a.cfg.HostWrites.Enabled {
451 return nil, ErrCmdClaim
452 }
453 if rt.Kind != "instance" {
454 return nil, ErrCmdNotInstance
455 }
456 cb, ok := a.backend.(CmdBackend)
457 if !ok {
458 return nil, ErrCmdNotNested
459 }
460 if c, ok := a.backend.(Classifier); !ok {
461 return nil, ErrCmdNotNested
462 } else if l, _ := c.Layout(rt); l != LayoutNested {
463 return nil, ErrCmdNotNested
464 }
465 wctx, cancel := context.WithTimeout(ctx, cmdWakeBound)
466 defer cancel()
467 var release func()
468 for {
469 r, err := a.begin(wctx, rt, true)
470 if err == nil {
471 release = r
472 break
473 }
474 if errors.Is(err, errWakeTimeout) && wctx.Err() == nil {
475 continue // begin's own bound is 30 s; a run waits 60
476 }
477 if wctx.Err() != nil && ctx.Err() == nil {
478 err = errWakeTimeout
479 }
480 return nil, &CmdWakeError{Reason: memberSafeWakeReason(err), Err: err}
481 }
482 pctx, pcancel := context.WithCancel(ctx)
483 defer pcancel()
484 h := newCmdHold(spec.RunID, pcancel, a.now)
485 a.mu.Lock()
486 e := a.entries[rt.Host]
487 switch {
488 case e == nil || e.state != actAwake:
489 why := StopIdle
490 if e != nil && e.stopWhy != "" {
491 why = e.stopWhy
492 }
493 a.mu.Unlock()
494 release()
495 return nil, &CmdStoppedError{Reason: why}
496 case e.worker:
497 a.mu.Unlock()
498 release()
499 return nil, ErrCmdNotInstance
500 case cmdBusyLocked(e):
501 a.mu.Unlock()
502 release()
503 return nil, ErrCmdBusy
504 }
505 e.cmd = h
506 a.mu.Unlock()
507 release() // the hold, not the in-flight count, keeps the idle timer off now
508 run := &cmdRun{a: a, e: e, h: h}
509 abandon := func(err error) (CmdHandle, error) {
510 close(h.started)
511 run.Release()
512 return nil, err
513 }
514 var prepErr error
515 if spec.Prepare != nil {
516 prepErr = spec.Prepare(pctx)
517 }
518 a.mu.Lock()
519 // waited: the run has waited for a switch since launch eligibility was
520 // last checked, by Prepare or by spec.Recheck. A wait can be long, so
521 // eligibility is checked again before anything starts (re-review 5).
522 waited := false
523 for {
524 if e.state != actAwake {
525 why := e.stopWhy
526 a.mu.Unlock()
527 return abandon(&CmdStoppedError{Reason: why})
528 }
529 if prepErr != nil {
530 a.mu.Unlock()
531 return abandon(prepErr)
532 }
533 // ctx carries the run's deadline and cancellation (the box's run
534 // pass derives it from the record): a run cancelled or expired at
535 // any point of its admission never starts (re-review 5).
536 if err := ctx.Err(); err != nil {
537 a.mu.Unlock()
538 return abandon(err)
539 }
540 // Ownership, and a generation that is serving: the lease is held,
541 // no fence or shutdown holds the host, and the current
542 // generation's process is running.
543 if why := cmdOwnershipLocked(a, e); why != "" {
544 a.mu.Unlock()
545 return abandon(&CmdStoppedError{Reason: why})
546 }
547 // An overlap successor is on its way: the current generation may
548 // retire before the unit is even up, so nothing starts against it.
549 // The run waits for the switch (and then runs the successor's
550 // build) or for the successor's end (merge ruling M2). A stop
551 // cancels pctx, as do the run's own cancel and deadline, and the
552 // checks above then abandon the run.
553 if m := switchPendingLocked(e); m != nil {
554 a.mu.Unlock()
555 select {
556 case <-m.done:
557 case <-time.After(cmdSwitchPoll):
558 case <-pctx.Done():
559 }
560 waited = true
561 a.mu.Lock()
562 continue
563 }
564 if !waited || spec.Recheck == nil {
565 break
566 }
567 // The run's own eligibility, which Prepare checked before the
568 // wait, read again off a.mu (the record), then every check above
569 // once more, since the entry may have moved meanwhile.
570 a.mu.Unlock()
571 prepErr = spec.Recheck(pctx)
572 waited = false
573 a.mu.Lock()
574 }
575 // Admitted against the current generation, under the lock a swap's
576 // admission and switch take (merge ruling M2): no successor is live,
577 // and a swap admitted from here on is refused until StartCmd returns
578 // (cmdStartInFlightLocked).
579 h.starting = true
580 h.gen = e.gen
581 h.onProven = func() {
582 // The pin for h.gen is this run's alone once that generation is
583 // no longer the entry's current one.
584 a.mu.Lock()
585 stale := a.entries[e.host] != e || e.gen.N != h.gen.N
586 a.mu.Unlock()
587 if stale {
588 _ = a.persistGenerations()
589 }
590 }
591 // What the command runs is what the instance serves now: the current
592 // generation's recorded file, by path, never the live link, which a
593 // repoint, a fallback or a held update can move off it (zero-downtime
594 // spec §7.5). That holds whatever CARLOS_SWAP says, as it does for the
595 // legacy sidecar (settings ruling R-EXEC-SIDECAR): since settings Task
596 // 5c a flag-off wake runs its recorded file by path too, so a flag-off
597 // command on the live link could run a different build than its
598 // instance. Only a generation that names no file (a backend that cannot
599 // pin one) leaves the run the live link.
600 spec.serving = nil
601 if h.gen.StagedPath != "" {
602 spec.serving = &instancectl.StagedExe{Path: h.gen.StagedPath, Version: h.gen.Build, Digest: h.gen.Digest}
603 }
604 // And with the environment that generation launched with, from its
605 // handle: never the delivered files, which may hold a config revision
606 // it has not activated, a config swap queued or failed (merge ruling
607 // M3). A handle that kept none gets none, and StartCmd refuses.
608 spec.genEnv, spec.genKeyLayer = nil, instancekey.LayerNone
609 if lp, ok := e.proc.(launchEnvProc); ok {
610 spec.genEnv = lp.LaunchEnv()
611 }
612 if lp, ok := e.proc.(launchKeyLayerProc); ok {
613 spec.genKeyLayer = lp.LaunchKeyLayer()
614 }
615 a.mu.Unlock()
616 // One bound covers the build-state write and the start, so a stop
617 // waiting on the start still has the settle's budget after it
618 // (cmdStartBound).
619 sctx, scancel := context.WithTimeout(ctx, cmdStartBound)
620 defer scancel()
621 // The build about to touch the instance's database is recorded first
622 // (settings spec §4.2), as the sidecar's is (startSidecar): a command
623 // runs its serving generation's build against the data, and the build
624 // state must name that build before it does. A failed successor may
625 // have left the record naming its own build; without this write a
626 // command on the incumbent would run under a record that names
627 // another, and a held recovery could pick the wrong one. A write that
628 // fails refuses the launch (errBuildStateUnwritten): nothing starts.
629 //
630 // The starting hold is kept throughout. A stop that begins meanwhile
631 // waits for this start to return, and no overlap swap is admitted
632 // (cmdStartInFlightLocked), so the generation recorded is still the
633 // one serving when the unit starts. The write takes time, so
634 // ownership and the run's own cancel and deadline are checked again
635 // after it, before anything starts.
636 if err := a.recordBuild(sctx, rt.Host, h.gen.Build); err != nil {
637 return abandon(err)
638 }
639 a.mu.Lock()
640 var refused error
641 switch {
642 case e.state != actAwake:
643 refused = &CmdStoppedError{Reason: e.stopWhy}
644 case ctx.Err() != nil:
645 refused = ctx.Err()
646 case e.gen.N != h.gen.N:
647 // Not reached while the starting hold keeps swaps out; a
648 // generation that changed anyway is never started against.
649 refused = errSwapBusy
650 default:
651 if why := cmdOwnershipLocked(a, e); why != "" {
652 refused = &CmdStoppedError{Reason: why}
653 }
654 }
655 a.mu.Unlock()
656 if refused != nil {
657 return abandon(refused)
658 }
659 u, err := cb.StartCmd(sctx, rt, spec)
660 h.unit = u
661 close(h.started)
662 if err != nil {
663 if u != nil {
664 // The start reached the control service, so the unit may be
665 // running: settle it now. Release keeps it for the next stop
666 // if it does not settle.
667 sctx, scancel := context.WithTimeout(context.WithoutCancel(ctx), cmdStopBound)
668 _ = h.stop(sctx, "")
669 scancel()
670 }
671 run.Release()
672 return nil, err
673 }
674 a.mu.Lock()
675 stopping, why := e.state != actAwake, e.stopWhy
676 a.mu.Unlock()
677 if stopping {
678 // A stop began during the start. finishStop is waiting to stop the
679 // unit, unless its wait already timed out: stop it here too (once
680 // either way), so a late start never outlives the entry.
681 go h.stop(context.Background(), why) //nolint:errcheck
682 }
683 return run, nil
684}
685