Skip to content

feat(jobs): process-group kill for detached jobs; truncate list COMMAND column#153

Merged
acmore merged 3 commits into
mainfrom
feat/jobs-group-kill
Jul 5, 2026
Merged

feat(jobs): process-group kill for detached jobs; truncate list COMMAND column#153
acmore merged 3 commits into
mainfrom
feat/jobs-group-kill

Conversation

@acmore

@acmore acmore commented Jul 5, 2026

Copy link
Copy Markdown
Owner

Two fixes for multi-pod detached-job ergonomics.

1. Process-group kill (the zombie-process problem)

Problem. Detached jobs had no process-group identity: the wrapper launched the command with (exec "$@") & and jobs stop sent signals to that single PID only. When a rank crashed, sibling ranks' children (pretrain.py under torchrun) survived the leader and kept GPU memory allocated — the next job launch then failed with CUDA OOM. Killing required a second manual pass plus polling nvidia-smi.

Change.

  • The detach wrapper launches the user command via setsid, so it leads its own process group (pgid == pid, recorded in job metadata). Images without setsid fall back to the old behavior with pgid=0 and a notice in the job log.
  • The jobs scan now also reports groupLive — the count of live (non-zombie) processes still in the job's group, identity-verified via the inherited OKDEV_JOB_ID env marker (a pgid number cannot be recycled while any member lives, so a mismatch means an unrelated group and is never signaled).
  • jobs stop signals the group (kill -SIG -- -pgid) instead of the leader, treats "metadata terminal but group members alive" as still-running (this is exactly the leader-died-children-linger case), escalates TERM→KILL as before, and only reports success once the group is drained — a clean exit now means the process tree and its GPU memory are gone.
  • Compatibility: old jobs (no pgid in metadata), sidecar-less containers, and mixed CLI versions all degrade to the previous leader-only path; the scan-line format change (<alive>\t<groupLive>\t<json>) parses all older shapes.

2. jobs list COMMAND column truncation

Long training commands wrapped table rows, breaking one-line-per-job readability. On a terminal the COMMAND column is now truncated with to the remaining terminal width (floor of 16 chars); piped/redirected output and --json keep the full command. --json also exposes pgid/groupLive per pod state.

Testing

  • Unit: wrapper setsid branch + pgid placeholders, scan-line parsing (3-field/2-field/raw), group signal script + legacy fallback, stop straggler scenarios (group TERM after leader exit; non-zero exit when the group never drains), needs-signal/settled criteria, width-limit and truncation helpers.
  • Linux e2e (runs in CI): launches the real detach launcher with a sh -c 'sleep 300 & sleep 300 & wait' tree, asserts pgid==pid in metadata, kills via the real signal script, and polls /proc until every group member is gone.
  • gofmt -l clean; go test -race ./... passes (e2e skips on darwin).

🤖 Generated with Claude Code

acmore and others added 3 commits July 5, 2026 13:21
…ND column

Detached jobs now launch via setsid so the user command leads its own
process group (pgid recorded in metadata). `jobs stop` signals the whole
group — covering children like torchrun workers whose leader already
exited and would previously linger holding GPU memory — and only reports
success once no live group member remains. Group identity is verified via
the inherited OKDEV_JOB_ID env marker before signaling, so recycled
pids/pgids are never hit. Containers without setsid and jobs from older
okdev versions fall back to the previous leader-only behavior.

The scan line gains a groupLive count ("<alive>\t<groupLive>\t<json>",
older shapes still parse) so stop's terminal criterion is "metadata
terminal AND group drained"; jobs list --json exposes pgid/groupLive.

jobs list also truncates the COMMAND column to the terminal width on TTY
output (piped output and --json keep the full command) so long training
commands no longer wrap table rows.

Includes a Linux-only end-to-end test (CI) that launches a real process
tree, kills the group through the actual signal script, and asserts every
member is gone.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ibility

dash's kill builtin rejects `kill -SIG -- -pgid` with "Illegal number: -";
the bare "-<pgid>" operand after the signal option works in dash, bash,
zsh, and busybox ash (verified against real process groups in all three
local shells; CI's /bin/sh is dash).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…back in kind smoke

Four scenarios on a real pod: detach logs land under /var/okdev/exec;
jobs list --output json reports pgid and live group members; jobs stop
kills the whole process tree (verified by scanning /proc for survivors);
and after crashing the dev container into CrashLoopBackOff, jobs logs
still returns the job's output through the okdev-sidecar fallback.

The container-crash step runs last before teardown since it leaves the
dev container crashlooping. The long sleeps are nominal workloads — the
group kill ends them in seconds, so wall-clock cost is the crash-loop
induction (~30-40s).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@acmore acmore merged commit 2020408 into main Jul 5, 2026
2 checks passed
@acmore acmore deleted the feat/jobs-group-kill branch July 5, 2026 06:36
acmore added a commit that referenced this pull request Jul 5, 2026
…ND column (#153)

* feat(jobs): process-group kill for detached jobs; truncate list COMMAND column

Detached jobs now launch via setsid so the user command leads its own
process group (pgid recorded in metadata). `jobs stop` signals the whole
group — covering children like torchrun workers whose leader already
exited and would previously linger holding GPU memory — and only reports
success once no live group member remains. Group identity is verified via
the inherited OKDEV_JOB_ID env marker before signaling, so recycled
pids/pgids are never hit. Containers without setsid and jobs from older
okdev versions fall back to the previous leader-only behavior.

The scan line gains a groupLive count ("<alive>\t<groupLive>\t<json>",
older shapes still parse) so stop's terminal criterion is "metadata
terminal AND group drained"; jobs list --json exposes pgid/groupLive.

jobs list also truncates the COMMAND column to the terminal width on TTY
output (piped output and --json keep the full command) so long training
commands no longer wrap table rows.

Includes a Linux-only end-to-end test (CI) that launches a real process
tree, kills the group through the actual signal script, and asserts every
member is gone.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(jobs): drop -- before negative pgid in group kill for dash compatibility

dash's kill builtin rejects `kill -SIG -- -pgid` with "Illegal number: -";
the bare "-<pgid>" operand after the signal option works in dash, bash,
zsh, and busybox ash (verified against real process groups in all three
local shells; CI's /bin/sh is dash).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(e2e): cover detach runtime-volume logs, group stop, sidecar fallback in kind smoke

Four scenarios on a real pod: detach logs land under /var/okdev/exec;
jobs list --output json reports pgid and live group members; jobs stop
kills the whole process tree (verified by scanning /proc for survivors);
and after crashing the dev container into CrashLoopBackOff, jobs logs
still returns the job's output through the okdev-sidecar fallback.

The container-crash step runs last before teardown since it leaves the
dev container crashlooping. The long sleeps are nominal workloads — the
group kill ends them in seconds, so wall-clock cost is the crash-loop
induction (~30-40s).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant