Add units RFC: supervised sessions with lifecycle policy - #94
Conversation
Design-only. Proposes declarative units (systemd-shaped, INI, on-disk, autostart) layered on three primitives that fix existing bugs: group kill, server-enforced deadlines/leases, and GC of exited PTY slots.
Coverage
|
|
… weaknesses The worked-example commit accidentally deleted Pipe backing, Wire and CLI, and Server restart. Restores them, corrects the refuse_lsp_message citation, gives units an explicit identity field rather than reusing the client-chosen tag, and records the design's known weak points.
…ecStop/StartPre - Health gating moves out of Type= into ActiveWhenHealthy= so systemd's Type= semantics stay intact. - Requires= carries systemd's stop propagation rather than a narrowed meaning; a cyclic edit rejects one file, not the whole graph. - Adds ExecStartPre=/ExecStop= and defines helper-child accounting, including SIGCHLD pid dispatch against the existing pty pid registry. - Specifies ReadyMatch's stream, splitting, and bound. - Unit sessions are exempt from max_ptys; max-units bounds them instead. - Persists operator intent (disabled, manually stopped) in KV behind a new server-owned blit/ prefix. - Adds a platform support matrix and a testability section.
|
🐮 Moo review — @indent, everything below is up for pushback. These are review hypotheses and suggested tradeoffs, not decrees; please challenge any premise, severity, or alternative that does not fit the intended shape of blit. I reviewed PR head Highest-priority design issues1. A unit cannot be only policy attached to an existing
|
…rammar, threat model - A unit is not policy on a Pty: adds an explicit UnitRuntime registry, since units exist while inactive/blocked/RemainAfterExit. The PTY stays the only stream object. - Adds generations and an ordered restart sequence, so SIGCHLD-triggered restarts cannot let a retired reader mutate a new generation. - SIGCHLD wakes the supervisor for targeted waitpid over owned pids; the global waitpid(-1) drain is deleted, not rescheduled, so it cannot steal ExecHealthCheck/audio/LSP statuses. - Deadlines become independent causes with an enforced minimum, plus server-minted lease ids, epochs, and single-holder reclaim. - Replaces 'man systemd.service is the reference' with an explicit strict grammar; only the vocabulary transfers. - Security: unit dirs excluded from the fs family and install made a local capability. Mode checks do not distinguish a remote fs writer from the operator, which is the attacker the section named. - State machine becomes a transition table; health failures thresholded; After= documented as an initial gate; live-reload semantics defined; unsupported platform capabilities are load errors, not warnings. - Splits unit delivery into core and policy stages.
|
Strong review. Six of these are things I got wrong rather than tradeoffs I chose, and I've taken them. Addressed in 8375171 (you reviewed Where I push back is narrower than you offered, and it's mostly about the two decisions that were made explicitly upstream of the RFC. Taken1. A unit is not policy on a 2. SIGCHLD must not reuse the global reaper. Confirmed against the source and this is a bug I was introducing: 3. Generations. Also right, and the sharpest finding. In-place respawn is safe today only because it happens after the EOF/exited path; restarting on leader death removes that precondition and I hadn't replaced it. Added 4. Deadlines and leases don't compose. "All resolve to one enforcement path" was hand-waving. Now independent causes with 5. The security control didn't address its own attacker. Completely right and this was the worst of them. A remote Also taken: state machine is now a transition table covering spawn failure, readiness timeout, health failure in both states, watchdog, explicit stop, config removal, stale generation, and limit exhaustion. Health failures are thresholded ( Pushback6. INI vs. TOML — the format is decided, the ambiguity is real. INI with systemd's key names was an explicit product decision upstream of this RFC, so I'm keeping it. But your substantive point stands and I've fixed it: "use So: only the vocabulary transfers, and there's now an explicit grammar table — no continuations, no inline comments, no quoting, Alternative B, partially. Deferring health checks and live reload conflicts with an explicit "readiness and health from day 1" decision, so I can't take it as written. But your underlying point — the first implementation should have a smaller race surface — is right, so I applied it as sequencing instead of scope: PR 4 is now unit core (registry, generations, strict parser, state machine, restart, Alternative A is already the plan — PRs 1-3 are exactly those primitives and land first. Alternative C is what the registry plus generations now implements. One correction to the review: the missing Pipe backing / Wire and CLI / Server restart sections were my splice error and were restored in |
Unit directories are no longer excluded from the fs family and install is no longer a local-only capability: remote install is intended. The security section states the equivalence instead — fs write access to a unit directory is durable code execution — and keeps BLIT_UNITS=0, load-time hygiene, and audit logging as the controls that actually earn their place. Also drops every 'an earlier draft said' retrospective (that belongs in the review thread), folds prose into tables, and cuts repeated rationale: 7744 -> 5812 words with no section or contract removed.
Design only — no code. Adds
docs/design/units.md.What this proposes
A unit is a named, declarative supervisor for a blit session: an on-disk file saying what to run, when to start it, when to restart it, when it is ready, when it is healthy, and what it depends on. Systemd's model, down to the key names.
The framing that keeps it blit-shaped: a unit introduces no new object.
C2S_RESTARTalready respawns an exited child in place, reusing the same pty id and driver (lib.rs:8683,pty_unix.rs:570), so blit already has a session identity that outlives a process. A unit is that identity made declarative — policy on an existingPtyentry, keyed by its existing tag. Clients subscribe once and follow a unit across restarts, with continuous scrollback as the blit-native journal. The alternative, a second process model bolted alongside the PTY map, is what the design exists to avoid.Three primitives first
Units sit on three changes that fix real bugs and are worth having alone:
blanket_frame_intervalreturnsNonewith no clients (lib.rs:2731), so the tick loop'snext_deadlinegoesNoneand a silent runaway is never visited on any schedule. The 5 sreap_zombiestask becomes a 1 s state-carrying supervisor tick.C2S_KILLsignals the leader pid only andC2S_CLOSE's SIGHUP misses anything that changed process group, even though the code already pgrp-signals for SIGWINCH (pty_unix.rs:224-230).KillMode=process-groupbecomes the default.ptys.removein the server is in theC2S_CLOSEarm,lib.rs:8883) andmax_ptysis hardcoded unlimited (crates/cli/src/main.rs:802). Adjacent standalone bug: hitting the cap is a barecontinuewith no reply, and there is no error opcode in the protocol at all, so a nonce-bearing create hangs forever.Decisions baked in
INI with sections and systemd's exact key names (four documented deviations); readiness and health both in v1;
Backing=pipestill feeds the alacritty driver so scrollback/search/COPY_RANGE/rendering keep working; the full deadline surface including connection leases that resolve into the deadline primitive rather than adding a second kill path.Health gates activation — departing from systemd, a unit with
ExecHealthCheck=reachesactiveonly after its first probe passes. That single rule makesAfter=mean "healthy" with no second ordering keyword, and withType=oneshot/surfaceit makes this expressible:Three different definitions of "up" — a
sd_notifyhandshake plus a passing health probe, a Wayland surface, a command that exited zero — and the dependent expresses its requirement without knowing which.Delivery
One axis per PR: (1) group kill, (2) supervisor tick + deadlines + leases, (3) GC +
max_ptys+S2C_CREATE_FAILED, (4) units. The first three are the incident-shaped fixes and stand alone.Every code claim in the doc is cited to
path:lineand verified againstdc6a265. Open questions are called out inline, notablyReadySurface=matching byapp_idrather than by process — surfaces are keyed by Wayland object id and the compositor records no client credentials today.Tag
@indentto continue the conversation here.