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 "fmt"
7 "log"
8
9 "github.com/carlosframework/platform/internal/cgroup"
10 "github.com/carlosframework/platform/internal/instancectl"
11 "github.com/carlosframework/platform/internal/registry"
12 "github.com/carlosframework/platform/internal/schedule"
13)
14
15var (
16 errSwapNoGens = errors.New("swap: backend cannot run staged generations")
17 errUnitNoGens = errors.New("swap: unit-backed routes keep stop-then-start")
18 errSwapLayout = errors.New("swap: this row's layout has no generation sockets")
19)
20
21// SwapBackend is what a swap needs from a backend beyond Backend and plan
22// 1's GenBackend (spec §7.3–§7.5). ExecBackend implements it on top of plan
23// 1's generation spawn and plan 2's containment. SplitBackend forwards it
24// for exec rows and refuses unit rows, which keep stop-then-start (spec §2
25// decision 9).
26type SwapBackend interface {
27 // StartStaged spawns generation g.N of rt from g.StagedPath, never
28 // through the live link, in its own child cgroup at g.Tier, on the
29 // generation's own socket (a foreign row: its canonical socket). It
30 // returns as soon as the process runs, with g's Socket and Control
31 // filled in, and never waits for health: the caller checks health
32 // through that handle under its own deadline, and stops (or kills) a
33 // process that fails. A nil Proc means nothing ran, unless the error
34 // says otherwise (startMayHaveRun; final review wave 2, N4): a start
35 // whose kill is unproven (errGenUnproven), or one handed to root whose
36 // outcome is uncertain (startUncertain).
37 //
38 // ctx may carry the caller's exec gate (withExecGate; final review F1).
39 // An implementation asks it (runExecGate) at its final launch point,
40 // after all of its preparation and immediately before the process or
41 // unit is started, and when it refuses starts nothing, cleans up what
42 // its preparation made, and returns the gate's error as it is with a
43 // nil Proc.
44 StartStaged(ctx context.Context, rt registry.Route, g Gen) (Gen, Proc, error)
45 // GenTier is build's own tier, from its staged manifest (spec §7.3).
46 // The swap captures it into Gen.Tier at admission.
47 GenTier(rt registry.Route, build string) cgroup.Limit
48 // GenReady runs once a pinned wake's generation is healthy: it widens
49 // the socket's mode (issue #103), as the live-link spawn's health tail
50 // does. A swap's successor gets the same from GenSwitched.
51 GenReady(rt registry.Route, g Gen, p Proc)
52 // RaiseGenParent sets the host's cgroup parent to the incumbent's tier
53 // plus the successor's, before the successor spawns (spec §7.3).
54 RaiseGenParent(ctx context.Context, rt registry.Route, incumbent, successor cgroup.Limit) error
55 // RestoreGenParent sets the parent back to tier, the current
56 // generation's captured tier, once every other generation is empty.
57 RestoreGenParent(ctx context.Context, rt registry.Route, tier cgroup.Limit) error
58 // GenSwitched runs under the entry lock at the switch (spec §7.5 step
59 // 5), before the in-memory switch. It points the canonical path at the
60 // new generation's socket (rev 15), purges the host's cached responses
61 // and widens the new socket's mode. An error refuses the switch.
62 GenSwitched(rt registry.Route, g Gen, p Proc) error
63}
64
65// execGateKey carries a staged spawn's exec gate on its context
66// (withExecGate).
67type execGateKey struct{}
68
69// withExecGate returns ctx carrying gate, the caller's composed ownership
70// and attempt checks for a staged spawn (final review F1): a planned wake's
71// B or its fallback, a hold's pinned build, an overlap successor, and every
72// worker start that has a gate (startWorker; Astra R3 §5 item 4). The
73// backend asks it at the very end of its preparation, right before the exec
74// (flat, and a worker) or the root start call (nested), so a lease lost, a
75// fence set or an attempt superseded while the backend read config, bound
76// sockets, wrote launch files or prepared the cgroup execs nothing. It rides
77// on the context, as withBeforeExec does, so StartStaged's and
78// StartWorkerStaged's signatures and every forwarding backend
79// (SplitBackend) stay as they are.
80func withExecGate(ctx context.Context, gate func() error) context.Context {
81 if gate == nil {
82 return ctx
83 }
84 return context.WithValue(ctx, execGateKey{}, gate)
85}
86
87// runExecGate asks ctx's exec gate, if it carries one, then writes ctx's
88// pending wake record, if it carries one (runPendingRecord, amadan #272),
89// and, once that write has run, asks the gate again (Astra review of
90// zdd-owed-items, P1): the lease, a fence or the attempt can change during
91// the bucket write, and the exec follows the second answer. A spawn with a
92// gate also honours its context there: one that has ended starts nothing.
93// With neither (every live-link spawn, the flag off) it is a no-op, so those
94// spawns are exactly as they were. Every refusal is marked launchRefused:
95// it is asked immediately before the exec, so nothing of the launch ran.
96func runExecGate(ctx context.Context) error {
97 if err := askExecGate(ctx); err != nil {
98 return launchRefused{err}
99 }
100 wrote, err := runPendingRecord(ctx)
101 if err != nil {
102 return launchRefused{err}
103 }
104 if wrote {
105 if err := askExecGate(ctx); err != nil {
106 return launchRefused{err}
107 }
108 }
109 return nil
110}
111
112// RunExecGate is runExecGate for a Backend implemented outside this
113// package: such a backend asks it immediately before its exec, as
114// ExecBackend does, so the caller's gate and the wake's pending record
115// (amadan #272) are honoured there.
116func RunExecGate(ctx context.Context) error { return runExecGate(ctx) }
117
118// askExecGate is runExecGate's gate alone.
119func askExecGate(ctx context.Context) error {
120 gate, ok := ctx.Value(execGateKey{}).(func() error)
121 if !ok || gate == nil {
122 return nil
123 }
124 if err := ctx.Err(); err != nil {
125 return err
126 }
127 return gate()
128}
129
130// launchGate is a flat staged spawn's (an instance generation's or a
131// worker's) before-start hook for the cgroup
132// manager (final review F1): launchHook (the test seam), then ctx's exec
133// gate, asked immediately before each start attempt, after the cgroup's
134// preparation, then ctx's pending wake record (runPendingRecord), and the
135// gate again once that write has run. Nil
136// when ctx carries no gate and no pending record and no test seam is set, so
137// a live-link spawn hands the manager no hook and starts exactly as it did.
138func (b *ExecBackend) launchGate(ctx context.Context, host string) func() error {
139 return b.launchGateWith(ctx, host, nil)
140}
141
142// launchGateWith is launchGate with live, a live-link spawn's LiveExecGate
143// check for the build it resolved (Task 9), asked after ctx's exec gate at
144// the same point, before the pending record and again after its write. A
145// nil live is launchGate
146// exactly, so a spawn on a box with no LiveExecGate hands the manager the
147// hook it always did.
148func (b *ExecBackend) launchGateWith(ctx context.Context, host string, live func() error) func() error {
149 if _, ok := ctx.Value(execGateKey{}).(func() error); !ok && b.launchHook == nil && live == nil && pendingRecordFrom(ctx) == nil {
150 return nil
151 }
152 return func() error {
153 if b.launchHook != nil {
154 b.launchHook(host)
155 }
156 if err := askExecGate(ctx); err != nil {
157 return err
158 }
159 if live != nil {
160 if err := live(); err != nil {
161 return err
162 }
163 }
164 wrote, err := runPendingRecord(ctx)
165 if err != nil || !wrote {
166 return err
167 }
168 // The write took time: the gate and the live check are asked again
169 // immediately before the exec (Astra review of zdd-owed-items, P1).
170 if err := askExecGate(ctx); err != nil {
171 return err
172 }
173 if live != nil {
174 return live()
175 }
176 return nil
177 }
178}
179
180// liveExecGateSetter is a backend that honours a live-link exec gate
181// (ExecBackend.LiveExecGate), or forwards it to one that does
182// (SplitBackend).
183type liveExecGateSetter interface {
184 SetLiveExecGate(gate func(rt registry.Route, build string) error)
185}
186
187var (
188 _ liveExecGateSetter = (*ExecBackend)(nil)
189 _ liveExecGateSetter = (*SplitBackend)(nil)
190)
191
192// SetLiveExecGate sets LiveExecGate. Call it before the backend spawns
193// anything, as Service.SetLiveExecGate does.
194func (b *ExecBackend) SetLiveExecGate(gate func(rt registry.Route, build string) error) {
195 b.LiveExecGate = gate
196}
197
198// SetLiveExecGate forwards to the exec half: unit-backed routes belong to
199// systemd, and the rollout never holds them (rollout.Managed).
200func (b *SplitBackend) SetLiveExecGate(gate func(rt registry.Route, build string) error) {
201 if s, ok := b.Exec.(liveExecGateSetter); ok {
202 s.SetLiveExecGate(gate)
203 }
204}
205
206// SetLiveExecGate installs gate as the backend's LiveExecGate and as the
207// activator's own check at a worker's converge and recycle, which stop the
208// old child before the backend's spawn could refuse the new one. Only a box
209// that declined CARLOS_SWAP=overlap and honours failure records or hold-ran
210// markers calls it (internal/host, honourDeclinedFailures), and before
211// Start, like Planner. gate is asked under a.mu, so it must answer from
212// memory, the row and the live link, and never call back into Service.
213func (a *Service) SetLiveExecGate(gate func(rt registry.Route, build string) error) {
214 a.mu.Lock()
215 a.liveExecGate = gate
216 a.mu.Unlock()
217 if s, ok := a.backend.(liveExecGateSetter); ok {
218 s.SetLiveExecGate(gate)
219 }
220}
221
222// StartStaged is plan 1's StartGen with perGen true, except for the
223// executable, the tier and the health wait, which is the caller's. A flat
224// child execs g.StagedPath directly. A nested unit's executable mount names
225// it, which is Task 9's leg; until then a nested row is refused. A foreign
226// row keeps its canonical socket (plan 1): it gets one generation at a time
227// (a cold, pinned or hold start), and Swap never overlaps it. A flat row
228// whose generation path would not fit a unix socket path is handled the
229// same way (genLayout, rev 19 rule 4).
230func (b *ExecBackend) StartStaged(ctx context.Context, rt registry.Route, g Gen) (Gen, Proc, error) {
231 if g.Tier == (cgroup.Limit{}) {
232 g.Tier = b.GenTier(rt, g.Build)
233 }
234 p := b.pathsFor(rt)
235 switch genLayout(p.layout, rt.Host) {
236 case LayoutFlat:
237 sock := b.flatGenSocket(rt.Host, g.N)
238 if err := clearOwnGenSocket(sock); err != nil {
239 return g, nil, fmt.Errorf("%s: generation socket: %w", rt.Host, err)
240 }
241 proc, _, err := b.spawnChildFrom(ctx, rt, sock, g.N, g.StagedPath, g.Tier)
242 if err != nil {
243 return g, proc, err
244 }
245 g.Socket, g.Control = sock, sock+".control"
246 return g, proc, nil
247 case LayoutForeign:
248 // As StartGen does for a canonical-socket spawn: a link left there
249 // by an agent that ran with generations on would fail the app's bind.
250 if err := clearLeftoverCanonicalLink(rt.Socket); err != nil {
251 return g, nil, fmt.Errorf("%s: leftover canonical link: %w", rt.Host, err)
252 }
253 proc, _, err := b.spawnChildFrom(ctx, rt, rt.Socket, g.N, g.StagedPath, g.Tier)
254 if err != nil {
255 return g, proc, err
256 }
257 g.Socket, g.Control = rt.Socket, rt.Socket+".control"
258 return g, proc, nil
259 case LayoutNested:
260 sock := nestedGenSocketIn(p.runDir, g.N)
261 // A start with no staged path (a cold wake) is on the live link, as
262 // the flat arm is.
263 var exe *instancectl.StagedExe
264 if g.StagedPath != "" {
265 exe = &instancectl.StagedExe{Path: g.StagedPath, Version: g.Build, Digest: g.Digest}
266 }
267 proc, _, err := b.spawnUnitFrom(ctx, rt, sock, g.N, exe, g.Tier)
268 if err != nil {
269 return g, proc, err
270 }
271 g.Socket, g.Control = sock, sock+".control"
272 return g, proc, nil
273 case LayoutFenced, LayoutConflict:
274 return g, nil, fmt.Errorf("%s: refusing to spawn: %s", rt.Host, p.reason)
275 }
276 return g, nil, errSwapLayout
277}
278
279// GenTier is build's own tier: TierOf when it is wired (the agent reads
280// the build's staged manifest), the live tier otherwise.
281func (b *ExecBackend) GenTier(rt registry.Route, build string) cgroup.Limit {
282 if b.TierOf != nil && build != "" {
283 return b.TierOf(rt, build)
284 }
285 return b.tier(rt)
286}
287
288// GenReady widens a healthy pinned generation's socket mode (issue #103).
289func (b *ExecBackend) GenReady(rt registry.Route, g Gen, p Proc) {
290 b.normalizeSocketMode(rt, g.Socket, p)
291}
292
293// RaiseGenParent sets the parent to the incumbent's tier plus the
294// successor's (spec §7.3, plan 2 D3), each captured from its own artifact
295// at the attempt's admission, through plan 2's ParentLimit. That keeps the
296// parent's expected-limit record in step.
297func (b *ExecBackend) RaiseGenParent(ctx context.Context, rt registry.Route, incumbent, successor cgroup.Limit) error {
298 return b.ParentLimit(ctx, rt, cgroup.OverlapLimit(incumbent, successor))
299}
300
301// RestoreGenParent puts the parent back at the current generation's tier.
302func (b *ExecBackend) RestoreGenParent(ctx context.Context, rt registry.Route, tier cgroup.Limit) error {
303 return b.ParentLimit(ctx, rt, tier)
304}
305
306// GenSwitched points the canonical socket path at g's socket first, with
307// plan 1's atomic helper. A failure refuses the switch, because outside
308// dialers (#103) would otherwise be left on a process that is about to
309// drain. Then it drops the host's cached responses (plan 1's generation tag
310// already makes them miss; this reclaims the space) and widens the new
311// socket to 0660 (issue #103).
312//
313// Lock contract (plan 1, Task 4). The caller holds a.mu, the lock under
314// which plan 1 creates and removes the link, and sets e.gen to g in the
315// same critical section. So finishStop always removes the link that names
316// the generation it retires, and one entry's stop can never delete the
317// next entry's link.
318func (b *ExecBackend) GenSwitched(rt registry.Route, g Gen, p Proc) error {
319 if !linksCanonical(rt, g) {
320 return errSwapLayout
321 }
322 if err := swapCanonicalLink(rt.Socket, g.Socket); err != nil {
323 return fmt.Errorf("%s: canonical socket link: %w", rt.Host, err)
324 }
325 if b.Cache != nil {
326 if n := b.Cache.PurgeHost(rt.Host); n > 0 {
327 log.Printf("activator: purged %d cached response(s) for %s at the switch to generation %d", n, rt.Host, g.N)
328 }
329 }
330 b.normalizeSocketMode(rt, g.Socket, p)
331 return nil
332}
333
334// genExec picks the exec half for a swap operation. Unit rows refuse, and
335// so does an exec half that cannot run staged generations.
336func (b *SplitBackend) genExec(rt registry.Route) (SwapBackend, error) {
337 if rt.Backing == registry.BackingUnit {
338 return nil, errUnitNoGens
339 }
340 g, ok := b.Exec.(SwapBackend)
341 if !ok {
342 return nil, errSwapNoGens
343 }
344 return g, nil
345}
346
347func (b *SplitBackend) StartStaged(ctx context.Context, rt registry.Route, g Gen) (Gen, Proc, error) {
348 gb, err := b.genExec(rt)
349 if err != nil {
350 return g, nil, err
351 }
352 return gb.StartStaged(ctx, rt, g)
353}
354
355func (b *SplitBackend) GenTier(rt registry.Route, build string) cgroup.Limit {
356 gb, err := b.genExec(rt)
357 if err != nil {
358 return cgroup.TierDefault
359 }
360 return gb.GenTier(rt, build)
361}
362
363func (b *SplitBackend) GenReady(rt registry.Route, g Gen, p Proc) {
364 if gb, err := b.genExec(rt); err == nil {
365 gb.GenReady(rt, g, p)
366 }
367}
368
369func (b *SplitBackend) RaiseGenParent(ctx context.Context, rt registry.Route, incumbent, successor cgroup.Limit) error {
370 gb, err := b.genExec(rt)
371 if err != nil {
372 return err
373 }
374 return gb.RaiseGenParent(ctx, rt, incumbent, successor)
375}
376
377func (b *SplitBackend) RestoreGenParent(ctx context.Context, rt registry.Route, tier cgroup.Limit) error {
378 gb, err := b.genExec(rt)
379 if err != nil {
380 return err
381 }
382 return gb.RestoreGenParent(ctx, rt, tier)
383}
384
385func (b *SplitBackend) GenSwitched(rt registry.Route, g Gen, p Proc) error {
386 gb, err := b.genExec(rt)
387 if err != nil {
388 return err
389 }
390 return gb.GenSwitched(rt, g, p)
391}
392
393var errNestedSidecarGen = errors.New("nested sidecar: the generation has no staged executable")
394
395// SidecarGenStarter starts a legacy co-spawned sidecar from a generation's
396// captured staged path (spec §7.5: "Every legacy co-spawned sidecar runs
397// its instance's serving artifact").
398type SidecarGenStarter interface {
399 StartSidecarGen(ctx context.Context, rt registry.Route, g Gen) (Proc, error)
400}
401
402// StartSidecarGen execs g.StagedPath for a flat (or foreign) host. A
403// nested host's sidecar unit binds what root lays out for it, so the agent
404// names the generation's staged executable and root validates it. A
405// generation with none (it was woken on the live link) refuses. It never
406// reads the live link to decide: a repoint can land between that read and
407// the start, and after a fallback the live link names a build the instance
408// is not serving. The supervisor retries with its backoff.
409func (b *ExecBackend) StartSidecarGen(ctx context.Context, rt registry.Route, g Gen) (Proc, error) {
410 p := b.pathsFor(rt)
411 switch p.layout {
412 case LayoutNested:
413 // The unit binds what root lays out, so root is told the
414 // generation's staged build and validates it (Task 9). Nothing here
415 // reads the live link: a fallback or a repoint cannot change what
416 // this sidecar runs.
417 if g.StagedPath == "" {
418 return nil, errNestedSidecarGen
419 }
420 exe := instancectl.StagedExe{Path: g.StagedPath, Version: g.Build, Digest: g.Digest}
421 return b.startSidecarUnitFrom(ctx, rt, &exe, g.Tier)
422 case LayoutFenced, LayoutConflict:
423 return nil, fmt.Errorf("%s: refusing to start sidecar: %s", rt.Host, p.reason)
424 }
425 return b.startSidecarChildFrom(ctx, rt, g.StagedPath)
426}
427
428// StartSidecarGen forwards to the exec half for exec rows. A unit row keeps
429// its own StartSidecar.
430func (b *SplitBackend) StartSidecarGen(ctx context.Context, rt registry.Route, g Gen) (Proc, error) {
431 if rt.Backing != registry.BackingUnit {
432 if s, ok := b.Exec.(SidecarGenStarter); ok {
433 return s.StartSidecarGen(ctx, rt, g)
434 }
435 }
436 return b.pick(rt).StartSidecar(ctx, rt)
437}
438
439// KillSidecar kills whatever is left of the host's legacy sidecar and
440// returns once its cgroup is empty (zero-downtime spec §7.4, §7.5): the
441// leader's exit says nothing of a process it forked. Flat with containment:
442// the role-sidecar child (plan 2 D4). Nested: the sidecar unit's stop, which
443// returns once systemd has emptied its cgroup (KillMode=mixed sends SIGKILL
444// to whatever SIGTERM left). Without containment the sidecar shares the
445// host cgroup with the instance, and the terminal stop's evidence on that
446// cgroup is the proof (errNoGenContainment, as KillGen answers).
447//
448// The role child is kept after the kill, and the next sidecar start
449// (StartChildGated) removes it and makes it fresh first, or refuses: a
450// cgroup that took cgroup.kill is never spawned into again (amadan #272,
451// internal/cgroup/recreate_linux.go).
452func (b *ExecBackend) KillSidecar(ctx context.Context, rt registry.Route) error {
453 switch p := b.pathsFor(rt); p.layout {
454 case LayoutNested:
455 if b.Control == nil {
456 return fmt.Errorf("%s: no instance control channel to prove the sidecar unit stopped", rt.Host)
457 }
458 return b.Control.Stop(ctx, rt.Host, instancectl.RoleSidecar)
459 case LayoutFenced, LayoutConflict:
460 return fmt.Errorf("%s: refusing to kill a sidecar: %s", rt.Host, p.reason)
461 }
462 if !b.GenContainment {
463 return errNoGenContainment
464 }
465 if b.Cgroups == nil {
466 return cgroup.ErrNoDelegation
467 }
468 c, err := b.Cgroups.OpenCleanupChild(rt.Host, cgroup.RoleChild("sidecar"))
469 if errors.Is(err, cgroup.ErrNoCgroup) {
470 return nil // removed after the leader's exit, so empty
471 }
472 if err != nil {
473 return err
474 }
475 if err := c.KillAndEmpty(ctx); err != nil {
476 _ = c.Close()
477 return err
478 }
479 return c.Close()
480}
481
482// KillSidecar forwards to the exec half for exec rows; systemd owns a unit
483// row's tree.
484func (b *SplitBackend) KillSidecar(ctx context.Context, rt registry.Route) error {
485 if rt.Backing == registry.BackingUnit {
486 return nil
487 }
488 if k, ok := b.Exec.(sidecarKiller); ok {
489 return k.KillSidecar(ctx, rt)
490 }
491 return nil
492}
493
494// writeFence is MountGated's fence for the control socket of host's
495// generation gen. It is nil when the backend has no ScheduleFence seam.
496func (b *ExecBackend) writeFence(host string, gen uint64) schedule.WriteFence {
497 if b.ScheduleFence == nil {
498 return nil
499 }
500 return func(write func()) bool { return b.ScheduleFence(host, gen, write) }
501}
502
503// genLayouter is a backend whose rows can read differently to generations
504// than to Classify (genLayout, rev 19 rule 4). The swap's layout decisions
505// ask it before Classifier (Service.swapLayout).
506type genLayouter interface {
507 GenLayout(rt registry.Route) Layout
508}
509
510var (
511 _ genLayouter = (*ExecBackend)(nil)
512 _ genLayouter = (*SplitBackend)(nil)
513)
514
515// GenLayout is Layout as generations see it: a flat row whose generation
516// path would not fit a unix socket path reads foreign (genLayout).
517func (b *ExecBackend) GenLayout(rt registry.Route) Layout {
518 l, _ := b.Layout(rt)
519 return genLayout(l, rt.Host)
520}
521
522// GenLayout forwards to the exec half for exec rows. A unit row reads as
523// its Layout does.
524func (b *SplitBackend) GenLayout(rt registry.Route) Layout {
525 if rt.Backing != registry.BackingUnit {
526 if g, ok := b.Exec.(genLayouter); ok {
527 return g.GenLayout(rt)
528 }
529 }
530 l, _ := b.Layout(rt)
531 return l
532}
533
534// genPather is a backend that can name a generation's sockets.
535type genPather interface {
536 GenPaths(rt registry.Route, n uint64) (Gen, bool)
537}
538
539// GenPaths is the generation socket pair a spawn of generation n of rt
540// binds, as rev 19 lays them out. It reports false for a row with no
541// generation path (foreign, fenced, conflict, and a flat row whose
542// generation path would not fit a unix socket path, genLayout).
543func (b *ExecBackend) GenPaths(rt registry.Route, n uint64) (Gen, bool) {
544 p := b.pathsFor(rt)
545 var sock string
546 switch genLayout(p.layout, rt.Host) {
547 case LayoutFlat:
548 sock = b.flatGenSocket(rt.Host, n)
549 case LayoutNested:
550 sock = nestedGenSocketIn(p.runDir, n)
551 default:
552 return Gen{}, false
553 }
554 return Gen{N: n, Socket: sock, Control: sock + ".control"}, true
555}
556
557// GenPaths forwards to the exec half for exec rows. A unit row has no
558// generation path of its own.
559func (b *SplitBackend) GenPaths(rt registry.Route, n uint64) (Gen, bool) {
560 if rt.Backing == registry.BackingUnit {
561 return Gen{}, false
562 }
563 if gp, ok := b.Exec.(genPather); ok {
564 return gp.GenPaths(rt, n)
565 }
566 return Gen{}, false
567}
568
569// KillHost kills whatever is left in rt's own host cgroup and returns once
570// it is empty: a worker's termination proof (zero-downtime spec §7.4, §7.9).
571// It does not look at the row's layout: StartWorker spawns every worker
572// through spawnInCgroup into carlos-exec@<host>, whatever the layout says,
573// so a nested-looking worker is proven there too. An absent cgroup is
574// empty. No cgroup delegation at all is an unproven kill.
575//
576// The host cgroup is kept after the kill, and the worker's next start
577// (StartGated) removes it and makes it fresh first, or refuses: a cgroup
578// that took cgroup.kill is never spawned into again (amadan #272,
579// internal/cgroup/recreate_linux.go). That
580// holds with the flag off too, where only Task 15's F6 path reaches here.
581func (b *ExecBackend) KillHost(ctx context.Context, rt registry.Route) error {
582 if b.Cgroups == nil {
583 return cgroup.ErrNoDelegation
584 }
585 c, err := b.Cgroups.OpenCleanup(rt.Host)
586 if errors.Is(err, cgroup.ErrNoCgroup) {
587 return nil
588 }
589 if err != nil {
590 return err
591 }
592 if err := c.KillAndEmpty(ctx); err != nil {
593 _ = c.Close()
594 return err
595 }
596 return c.Close()
597}
598
599// KillHost dispatches like Start: a unit-backed row's one process tree is
600// systemd's, so there is no host cgroup of ours to kill.
601func (b *SplitBackend) KillHost(ctx context.Context, rt registry.Route) error {
602 if rt.Backing == registry.BackingUnit {
603 return nil // systemd owns the unit tenant's tree
604 }
605 if k, ok := b.Exec.(hostKiller); ok {
606 return k.KillHost(ctx, rt)
607 }
608 return nil
609}
610
611// EmptyHost proves rt's own host cgroup empty for a flag-off terminal stop
612// (Task 8, F6): it reads cgroup.events and kills only when something is
613// left, so a clean stop leaves no kill mark and nothing for the next start
614// to recreate. A populated cgroup goes through KillAndEmpty, which the
615// next start answers by making it fresh (Task 19). An absent cgroup is
616// empty; no delegated cgroup at all is cgroup.ErrNoDelegation, which the
617// caller treats as the leader-only proof. So is a cgroup with no
618// cgroup.kill (cgroup.ErrNoKill, a kernel before 5.14): nothing could kill
619// a descendant there, and reading it as an unproven kill would hold every
620// flag-off stop's lease on such a box.
621func (b *ExecBackend) EmptyHost(ctx context.Context, rt registry.Route) error {
622 open := b.openHostCleanup
623 if open == nil {
624 if b.Cgroups == nil {
625 return cgroup.ErrNoDelegation
626 }
627 open = b.Cgroups.OpenCleanup
628 }
629 c, err := open(rt.Host)
630 if errors.Is(err, cgroup.ErrNoCgroup) {
631 return nil
632 }
633 if errors.Is(err, cgroup.ErrNoKill) {
634 log.Printf("activator: %s: this kernel has no cgroup.kill, so the host cgroup cannot be killed; its leader's exit is the only proof there is", rt.Host)
635 return fmt.Errorf("%w: %w", cgroup.ErrNoDelegation, err)
636 }
637 if err != nil {
638 return err
639 }
640 populated, err := c.Populated()
641 if err == nil && populated {
642 err = c.KillAndEmpty(ctx)
643 }
644 if cerr := c.Close(); err == nil {
645 err = cerr
646 }
647 return err
648}
649
650// EmptyHost dispatches like KillHost: a unit-backed row has no host cgroup
651// of ours. An exec side without the read-first proof gets KillHost.
652func (b *SplitBackend) EmptyHost(ctx context.Context, rt registry.Route) error {
653 if rt.Backing == registry.BackingUnit {
654 return nil // systemd owns the unit tenant's tree
655 }
656 if e, ok := b.Exec.(hostEmptier); ok {
657 return e.EmptyHost(ctx, rt)
658 }
659 if k, ok := b.Exec.(hostKiller); ok {
660 return k.KillHost(ctx, rt)
661 }
662 return nil
663}
664