Let a script finish its teardown when the client goes away - #216
Merged
danielrmerskine merged 2 commits intoAug 11, 2026
Merged
Conversation
juul-charles-w
requested review from
adhanali,
cmfisher606 and
danielrmerskine
as code owners
August 6, 2026 01:46
Three coupled changes to what happens after a `lager python` client vanishes -- cancelled CI job, killed CLI, dropped network. This is the only cleanup path that survives the client being hard-killed, since it runs on the box and needs nothing from the client, so it is the backstop for every executor. Notice the client is gone. The streaming generator only checked for a dead peer when the script next wrote something, so a quiet script kept the bench for as long as it stayed quiet -- 12.8s in the worst measured case. It now yields a zero-length idle tick on every idle poll, and `peer_is_connected` reads the socket to distinguish a closed connection from an idle one. Detection is sub-second. Interrupt before terminating. `terminate_process` went straight to SIGTERM, whose default disposition kills the interpreter outright: no `finally`, no context-manager `__exit__`, no `atexit`. For a HIL script that is the difference between the bench being left idle and left live, because de-energising rails and releasing the DUT live in exactly those blocks. It now escalates SIGINT -> SIGTERM -> SIGKILL. Only the timeout(1) wrapper is signalled, not the script as well, for the double-delivery reason in the previous commit. Make the next job wait. Cleanup runs *after* the client has gone, so the box lock says nothing about whether the hardware is free -- the client releases it on the way out, and it lapses on its own when the client is killed. A job reaping a bench now publishes its PIDs for the duration (`lager.exec.quiesce`) and a starting job waits for that to clear. Without this, extending the cleanup window below would have widened the overlap it creates rather than closing it, which is why the three land together. The cleanup budget is an *idle* budget, not a total one. A fixed total cannot be set correctly here: it is a guess about how long someone else's teardown takes, and every value is wrong for someone. `wait_for_cleanup` instead escalates only after the script has gone CLEANUP_GRACE_S without observable progress, so a teardown that keeps working keeps its deadline pushed out while a wedged one is still cut off promptly. CLEANUP_MAX_S caps the total either way. Progress is CPU time *and* context switches across the script's process tree. CPU time alone was the wrong signal: hardware teardown is mostly blocking, and measured on hardware a single Acroname hub round trip takes ~2.2s while accruing about one 10ms clock tick, so sampling ticks read a working teardown as idle for up to 4.17s against a 3s budget. An end-to-end A/B put that combination at 3 of 11 interrupted teardowns silently truncated mid-cleanup. Adding context switches drops the worst observed quiet stretch to 1.12s; 11 of 11 then completed. CLEANUP_GRACE_S is 5.0s, roughly 4x that measured worst case. A process wedged in a single uninterruptible syscall moves neither counter, which is the case the watchdog exists to catch, so this widens what counts as progress without making a stuck process look busy. Verified on Linux. Co-authored-by: Cursor <cursoragent@cursor.com>
…-constant comment
danielrmerskine
force-pushed
the
feat/box-cleanup-quiesce
branch
from
August 11, 2026 22:18
6297387 to
b229ed7
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #215
No upstream write access, so this PR's base is
mainand its diff includes#215's commit. Review the second commit only, or merge #215 first and this
will shrink to it. GitHub will show the right thing once #215 lands.
Summary
Three coupled changes to what happens after a
lager pythonclient vanishes —cancelled CI job, killed CLI, dropped network. This is the only cleanup path
that survives the client being hard-killed, since it runs on the box and needs
nothing from the client, so it is the backstop for every executor.
Notice the client is gone. The streaming generator only checked for a dead
peer when the script next wrote something, so a quiet script kept the bench for
as long as it stayed quiet — 12.8s in the worst measured case. It now yields a
zero-length idle tick on every idle poll, and
peer_is_connectedreads thesocket to distinguish a closed connection from an idle one. Detection is
sub-second.
Interrupt before terminating.
terminate_processwent straight to SIGTERM,whose default disposition kills the interpreter outright: no
finally, nocontext-manager
__exit__, noatexit. For a HIL script that is the differencebetween the bench being left idle and left live, because de-energising rails and
releasing the DUT live in exactly those blocks. It now escalates
SIGINT → SIGTERM → SIGKILL.
Make the next job wait. Cleanup runs after the client has gone, so the box
lock says nothing about whether the hardware is free — the client releases it on
the way out, and it lapses on its own when the client is killed. A job reaping a
bench publishes its PIDs for the duration (
lager.exec.quiesce) and a startingjob waits for that to clear.
These three land together on purpose: without the gate, extending the cleanup
window would have widened the overlap it creates rather than closing it.
The cleanup budget is an idle budget
A fixed total cannot be set correctly from here — it is a guess about how long
someone else's teardown takes, and every value is wrong for someone. Ours is
~2.4s; a user with six LabJacks and three hubs could need ten times that, and a
number large enough for them means a wedged script holds a shared box that long.
wait_for_cleanupescalates only after the script has goneCLEANUP_GRACE_Swithout observable progress, so a working teardown keeps its deadline pushed
out while a wedged one is still cut off promptly.
CLEANUP_MAX_Scaps the totaleither way. This only has to exceed the longest pause within a teardown, which
is a far more stable quantity than its duration.
Why progress is context switches, not just CPU time
CPU time alone was the wrong signal, and this is the part worth scrutinising.
Hardware teardown is mostly blocking. Measured on hardware, a single Acroname
hub round trip takes ~2.2s and accrues about one 10ms clock tick over that
whole time, so sampling ticks reads a working teardown as idle:
Against the 3s budget that shipped, an end-to-end A/B on hardware put that
combination at 3 of 11 interrupted teardowns silently truncated mid-cleanup,
leaving the bench partly un-restored. With context switches added and the budget
at 5.0s (~4x the measured worst case), 11 of 11 completed.
A process wedged in a single uninterruptible syscall moves neither counter,
which is exactly the case the watchdog exists to catch — so this widens what
counts as progress without making a stuck process look busy. There is a test for
that, verified on Linux.
/proc/<pid>/schedstatwas measured as an alternativeand resolves no better (same underlying transaction boundaries), so this uses
status, which needs noCONFIG_SCHEDSTATS.Test plan
test_cleanup_watchdog.py,test_bench_quiesce.py,test_stream_disconnect.pyand updates totest_stream_teardown.py— 42tests, 5 procfs-gated (they run on CI).
test/unit/box test/unit/clirun: no new failures (the 5test_ssh_setup.pyones are a pre-existing local click version issue).Made with Cursor