fix: skip signal handlers outside main thread (#136)#138
Conversation
There was a problem hiding this comment.
Thanks for fixing the crash — the main-thread guard is the right call given signal.signal()'s constraint. One gap worth mentioning though:
The signal handlers are never recovered after a worker-thread-first import.
_install_handlers() runs only once, at import (_cleanup.py:135), and nothing else
in src/ ever calls it again. So in the exact scenario this PR targets — first import cwsandbox happening on a non-main thread — the signal handlers are
skipped and stay skipped for the life of the process. A re-import from the main thread is a cached no-op and doesn't re-run the install.
Concretely: a later SIGTERM (e.g. a k8s/orchestrator shutdown) terminates the process without running _cleanup — atexit doesn't fire on signal-induced
termination — so sandboxes get orphaned and keep consuming compute. That's the guarantee this module exists to provide.
I verified this with a runtime repro of the control flow: after a worker-thread install, _handlers_installed=False and SIGTERM/SIGINT are still at default
disposition; only an explicit main-thread call to _install_handlers() ever wires them up.
Two smaller notes:
- The PR description says handlers "can still be installed" from the main thread later — that recovery path doesn't actually exist in the code.
- The skip is logged at debug, so this degradation is silent. Worth bumping to warning.
Suggestion: the root cause is doing main-thread-only work (signal.signal) as an import-time side effect. Rather than installing at import, make installation lazy + idempotent and trigger it from the first real SDK use (e.g. _LoopManager.get() / Sandbox.run()). That attempts install on the main thread whenever the SDK is actually used, which delivers the recovery this PR describes — and ends up being less code than the current flag-juggling. Happy to discuss.
Closes #136
Summary
Importing
cwsandboxfrom a worker thread currently fails because_cleanup.pyinstalls SIGINT/SIGTERM handlers at import time, andsignal.signal()is only valid in the main thread.This keeps the
atexitcleanup registration available from any thread, but skips installing signal handlers when_install_handlers()runs outside the main thread. If the module is later initialized from the main thread, the signal handlers can still be installed.Verification
cwsandbox._cleanupno longer raisesValueError.pytest tests/unit/cwsandbox/test_cleanup.py -q: 18 passed.python -m ruff check src/cwsandbox/_cleanup.py tests/unit/cwsandbox/test_cleanup.py: all checks passed.