Skip to content

Design: per-module cgroup placement of supervised children (Linux-only, placement not policy) — oomd currently has one candidate: the daemon #97

Description

@iceteaSA

Summary

Every supervised module and every process a module spawns lives in ck-subc.service's single cgroup. On 2026-09-07 an aft tool shell's runaway (12 orphaned vitest workers, ~32 GiB swapped) sat in that cgroup, took the box to load 86 / memory PSI 84%, and was one systemd-oomd decision away from killing the daemon and all four modules — which is how the Aug-10 OOM went. Design question, not an ask-to-build: should the daemon place each supervised child in its own cgroup subtree so a module's runaway is bounded and killable without touching the daemon?

Live facts (icebox, daemon 0.17.20)

systemctl --user show ck-subc -p Delegate -p KillMode -p ManagedOOMMemoryPressure
Delegate=no
KillMode=control-group
ManagedOOMMemoryPressure=auto     # inherits app.slice
cgroup.subtree_control: (empty)

Flat cgroup. systemd-oomd selects its kill candidate among the descendants of the monitored slice; with no descendants under ck-subc.service, the candidate is the service itself. Kill granularity is the problem, not a memory cap.

What #16 already ruled, and why this is narrower

#16 ruled that core stays portable and does not carry box-vitals or memory-kill policy. This proposal is placement only: a Linux cfg island that writes the child's pid into a per-module cgroup at spawn, a no-op elsewhere, no wire change, no policy in core. Limits stay with the operator (unit drop-in or per-module config later, if ever).

Two halves — the daemon owns one

Tool shells are AFT's half. aft spawns them; it can spawn each into its own transient scope (systemd-run --user --scope --collect … on Linux, plain spawn elsewhere). The shell leaves ck-subc.service's cgroup and oomd kills the scope, never the daemon or aft. A per-module subtree cannot solve this case: aft's tool children would sit with aft, so killing the runaway kills aft's connection too. That half fixes the 09-07 incident by itself and is routed to aft separately.

A module's own runaway is the daemon's half. Per-module subtree:

Placement is the ruling I need

Either of:

  1. supervise.rs, behind #[cfg(target_os = "linux")], with a config flag defaulting off until the unit has Delegate=yes (writing cgroup.procs without delegation is EACCES; the daemon must degrade to today's behaviour, loudly once, not fail spawn).
  2. A launcher wrapper outside core (the daemon spawns ck-launch <module> which does the cgroup write then execs), keeping core free of cgroup knowledge at the cost of one more hop in the process tree and a second binary to attest.

I lean (1) for the child.wait() and provenance reasons, and will build whichever you rule. Explicit non-goals: no memory.high policy in core, no box-vitals surface, no behaviour change when delegation is absent.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions