fix: Reap JobRunner subprocesses before os._exit to prevent orphans and semaphore leaks#582
Open
hongquanli wants to merge 1 commit into
Open
fix: Reap JobRunner subprocesses before os._exit to prevent orphans and semaphore leaks#582hongquanli wants to merge 1 commit into
hongquanli wants to merge 1 commit into
Conversation
main_hcs.py exits via os._exit(), which skips multiprocessing's atexit hook — the one that would normally terminate daemon children. Any JobRunner subprocess still alive at that point (its shutdown event never set because the exit-time abort join timed out, the worker died before _finish_jobs, or the non-blocking shutdown threads lost the race) was orphaned to PPID 1 and leaked its queue/event semaphores (the "resource_tracker: leaked semaphore objects" warning users see). - Add shutdown_all_job_runners(): finds live JobRunner children via multiprocessing.active_children(), runs their full shutdown() in parallel, then terminates any survivor. Called in main_hcs.py right before os._exit(). - JobRunner.shutdown() now also clears _ready_event so its semaphore is released like the other primitives. - Fix the _finish_jobs comment that claimed "the OS will terminate subprocesses anyway" — untrue under os._exit. Verified: unit tests reap a runner whose shutdown event was never set; GUI driver runs (simulation) close normally and mid-acquisition with zero orphaned children and no resource_tracker warning. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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.
Summary
Fixes two user-visible symptoms of the same bug: orphaned
multiprocessing spawn_mainprocesses (PPID 1) accumulating after GUI sessions, and the shutdown warningresource_tracker: There appear to be N leaked semaphore objects to clean up at shutdown.Root cause
main_hcs.pyexits viaos._exit()(deliberate — PyQt5's C++ destructor order conflicts with Python's GC). Butos._exit()also skips multiprocessing's atexit hook, the mechanism that normally terminates daemon child processes. AnyJobRunnersubprocess still alive at that moment is orphaned to PPID 1 and its queue/event semaphores leak.The per-acquisition cleanup (
_finish_jobs) shuts runners down in non-blocking daemon threads, with a comment claiming "if app exits before threads complete, OS will terminate subprocesses anyway" — untrue underos._exit(). Runners stay alive whenever their shutdown event was never set: the exit-time abort join times out (15 s cap), the worker dies before_finish_jobs, or the shutdown threads lose the race toos._exit().Fix
shutdown_all_job_runners()(job_processing.py): finds liveJobRunnerchildren viamultiprocessing.active_children(), runs their fullshutdown()in parallel (bounded), then terminates any survivor. Called inmain_hcs.pyimmediately beforeos._exit().JobRunner.shutdown()now also clears_ready_event, releasing its semaphore like the other primitives._finish_jobscomment.Testing
tests/control/core/test_job_runner_teardown.py: a runner whose shutdown event was never set is reaped (deterministic reproduction of the leak condition); no-op with no runners; safe after a prior clean shutdown.test_job_runner_shutdown/pending, backpressure, zarr writer, OME-TIFF saving).main_hcs.py's exact exit path): normal close after an acquisition and close mid-acquisition — zero orphaned children, no resource_tracker warning.🤖 Generated with Claude Code