watch: serialize job runs and terminate in-flight jobs on shutdown - #119
watch: serialize job runs and terminate in-flight jobs on shutdown#119rsanheim wants to merge 1 commit into
Conversation
plur watch kept no handle on the job it spawned and never signalled it, so the job outlived plur on every shutdown path. Ctrl-C appeared to work only because the tty signals the whole foreground process group -- RSpec and Minitest both rescue Interrupt to print a summary, so in practice the suite survived and kept writing to the terminal after plur was gone. Verified against a `rescue Interrupt` Ruby job: SIGTERM and SIGINT both left it reparented to PID 1. Three related fixes: - ExecuteJob now Start/Waits and records the process, and TerminateRunningJob sends SIGTERM, waits a short grace, then SIGKILLs. It is called from the watch loop's defer (covering exit, signals, timeout, and error returns) and explicitly in reload() before syscall.Exec, where defers do not run and the exec'd image would otherwise inherit a child it never waits on. - A package-level mutex serializes runs. The debouncer resets its timer but cannot cancel an already-firing callback, and time.AfterFunc runs each on a fresh goroutine, so saves during a run started concurrent suites against the same test database -- five were observed from five saves two seconds apart. - The [Enter] run moves off the select loop. It ran inline, so signals were not serviced for the duration of the job and watch mode was unkillable from its own terminal; three genuine Ctrl-Cs over twelve seconds did nothing. The watch specs for manual-run output now delay `exit`, since it is honored immediately during a run rather than queuing behind it. Not addressed: grandchildren of a shell-wrapper job are still not reached, which would need process groups and signal forwarding.
|
Do not read the description above as accurate — the central claim is wrong and the code has a known regression. Not ready for review. What's wrongThe description claims Ctrl-C never really worked, on the grounds that RSpec and Minitest rescue Reading the actual sources, both of which were sitting on the machine:
Ctrl-C is the runners' own clean shutdown and it works today on main. The regression this commit introduces
So as it stands this fixes the SIGTERM path and breaks the Ctrl-C path. What still holds, verified independently of that mistake
So the three P1s are real; what I got wrong was the trigger conditions for the orphan one, and the fix built on that wrong model. NextRework (uncommitted): wait when the terminal has already interrupted the job, send SIGINT ourselves when nothing has, escalate to SIGTERM then SIGKILL only if it doesn't stop. Verification is being moved onto the repo's existing tmux/PTY integration harness ( |
Found while auditing
plur watchshutdown/cleanup on the common user paths. Three P1s, all reproduced against a real watch session before and after.The problem
plur kept no handle on the job it spawned and never signalled it. Ctrl-C only appeared to work, because the tty signals the whole foreground process group — plur itself did nothing. RSpec and Minitest both
rescue Interruptto print a summary, so for real suites the job survives and keeps writing to your terminal after plur is gone.Verified with a
rescue InterruptRuby job, old binary:Reparented to PID 1 in both cases.
exitandreloadleak the same way; aftersyscall.Execthe job is still a child of the same pid, and the new image never waits on it, so it becomes a zombie — one per reload-with-running-job.Changes
Terminate in-flight jobs.
ExecuteJobnowStart/Waits and records the process;TerminateRunningJobsends SIGTERM, waits a 2s grace, then SIGKILLs. Called from the watch loop'sdefer(coversexit, signals, timeout, error returns) and explicitly inreload()beforesyscall.Exec, where defers don't run.Serialize runs. The debouncer resets its timer but can't cancel an already-firing callback, and
time.AfterFuncruns each on a fresh goroutine — so saves during a run started concurrent suites against the same test DB. Five concurrent runs were observed from five saves 2s apart. A package-level mutex inExecuteJobcovers both the debounced and[Enter]paths.[Enter]runs off the select loop. It ran inline, sosigChanwasn't serviced for the job's duration and watch mode was unkillable from its own terminal — three genuine Ctrl-Cs over 12s did nothing.After
(old binary on that last case: still alive after 12s, orphan survived)
Notes
start start start start end end end endinstead of alternating pairs.exitis now honored immediately during a manual run instead of queuing behind it, so they delay it. Their assertions are unchanged; added one covering the new prompt-exit behavior.Deliberately not done
Setpgid+kill(-pgid)would also reap grandchildren of a shell-wrapper job, but it stops the tty from delivering Ctrl-C to the job at all and makes plur responsible for forwarding — a much larger behavior change. This is the smaller fix; the grandchild limitation is noted in the code comment and written up separately.