Repository navigation
Replies: 1 comment 1 reply
|
Your reading of the gate is right, and it's already reported: #9220 describes the same thing on 0.2.0-rc.2 (246 active scopes at I checked against master
One nuance: a command that exits on its own normally doesn't hit this. Until a fix lands, stopping the leftover units is enough, and you don't need a restart: systemctl --user list-units --type=scope --state=active --plain --no-legend 'dsh-subprocess-*' 'dsh-terminal-*' \
| awk '{print $1}' \
| while read -r unit; do
if [ "$(systemctl --user show --property=TasksCurrent --value "$unit")" = 0 ]; then
systemctl --user stop "$unit"
fi
doneAfter the I've since run this under systemd 255 (Ubuntu 24.04) with a user manager: a scope whose command exits on its own is collected, a scope whose cgroup watch couldn't be added stays active at |
Uh oh!
There was an error while loading. Please reload this page.
Problem
SystemdScopeOwner.emptyRange()requiresterminationRequestedbefore it releases a leftover active-empty scope unit. That flag is only set bysignal()/terminate(). A command that completes on its own never issues a termination request, so:activewithTasksCurrent=0forever — systemd ends a scope only on the populated→empty transition, and a payload that never populated the cgroup leaves the unit active indefinitely (the class doc comment already documents this).waitForExit()observation loop keeps spawningsystemctl --user showfor that unit every 5 s — forever.liveset, retaining memory.Under sustained multi-agent usage (hundreds of tool calls) this accumulates. Observed on a long-running
dsh webhost: 900+ leakeddsh-subprocess-*scope units, ~500 concurrentsystemctlchildren at peak (spawn storm), 60% of one core burned insidechild_process.spawn, and heap growth from the retained handles. Clearing the stale units collapsed the storm instantly and CPU returned to idle.Fix
Drop the
terminationRequestedgate. The remaining predicate already proves the range is conclusively finished — the class's own doc comment argues exactly this: "a departed client cannot add another payload; a consumed request proves the payload already entered the scope." Either the direct client has departed (it forked every process it will ever fork) or the launch request was consumed (the payload entered the scope), and withTasksCurrent=0the managed range holds nothing. Termination signaling is orthogonal to settlement.aaa02a3970introducedemptyRange/releaseEmptyRangebut gated them onterminationRequested, which covers only the kill/terminate path; normal completion and launch failures leak.Verification
releases an active empty scope once its client has completed without a termination request— completes a scope with no signal and asserts the leftover unit is released (systemctl stopissued) and the observation settles.keeps waiting for an active empty scope no termination has requestedto... while its client is still alive— the wait invariant is client liveness, not absence of termination.subprocess-localsuite: 382 passed, 0 failed (also covers terminal scopes, spawn, windows-job suites).Posted per the contributing guide (bug reports go to Discussions; external PRs are not accepted). Context: observed on a long-running
dsh webhost driving an external agent team (0.2.1-rc.3 through current masterce7073d5— the gate is still present there). A fix is the one-line gate removal inemptyRange()plus dropping the now write-only field; I can share the exact patch in this thread if useful.All reactions