| 1 | package activator |
| 2 | |
| 3 | import ( |
| 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 | |
| 15 | var ( |
| 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). |
| 26 | type 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). |
| 67 | type 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. |
| 80 | func 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. |
| 96 | func 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. |
| 116 | func RunExecGate(ctx context.Context) error { return runExecGate(ctx) } |
| 117 | |
| 118 | // askExecGate is runExecGate's gate alone. |
| 119 | func 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. |
| 138 | func (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. |
| 148 | func (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). |
| 183 | type liveExecGateSetter interface { |
| 184 | SetLiveExecGate(gate func(rt registry.Route, build string) error) |
| 185 | } |
| 186 | |
| 187 | var ( |
| 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. |
| 194 | func (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). |
| 200 | func (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. |
| 213 | func (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). |
| 230 | func (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. |
| 281 | func (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). |
| 289 | func (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. |
| 297 | func (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. |
| 302 | func (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. |
| 318 | func (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. |
| 336 | func (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 | |
| 347 | func (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 | |
| 355 | func (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 | |
| 363 | func (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 | |
| 369 | func (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 | |
| 377 | func (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 | |
| 385 | func (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 | |
| 393 | var 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"). |
| 398 | type 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. |
| 409 | func (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. |
| 430 | func (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). |
| 452 | func (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. |
| 484 | func (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. |
| 496 | func (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). |
| 506 | type genLayouter interface { |
| 507 | GenLayout(rt registry.Route) Layout |
| 508 | } |
| 509 | |
| 510 | var ( |
| 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). |
| 517 | func (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. |
| 524 | func (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. |
| 535 | type 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). |
| 543 | func (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. |
| 559 | func (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. |
| 581 | func (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. |
| 601 | func (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. |
| 621 | func (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. |
| 652 | func (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 | |