Private
Public Access
0
0

fix(sigint): install SIGINT handler in AppController to drain pool on Ctrl+C

Ctrl+C in sloppy.py's terminal would hang the process when a worker of
the shared 4-thread I/O pool was mid-task in user code (e.g. a long-
running Gemini/Anthropic HTTP request). The hang chain:

  1. SIGINT delivered to main thread
  2. Python raises KeyboardInterrupt (default handler)
  3. Exception propagates out of main()
  4. Interpreter finalization begins
  5. ThreadPoolExecutor.__del__ runs shutdown(wait=True)
  6. shutdown(wait=True) joins all worker threads
  7. The blocked worker never returns -> hang

An atexit-based fix (mirroring the conftest fix at 8957c9a5) was
attempted first: register pool.shutdown(wait=False) at pool creation.
Verified empirically that this DOES NOT WORK — atexit handlers do not
fire at all when a pool worker is blocked in user code. The hang still
occurs in ThreadPoolExecutor.__del__ -> shutdown(wait=True).

Production fix: a SIGINT handler installed by AppController.__init__
that drains the pool non-blockingly and calls os._exit(0), bypassing
the broken finalization chain. One wire covers all three modes
(GUI/headless/web) since they all create an AppController.

Files:
- src/app_controller.py: new module-level _install_sigint_exit_handler
  helper called from __init__; one-line docstring at the function
  level documents the rationale.
- tests/test_app_controller_sigint.py: new test file with 2 regression
  tests (unit: handler is installed on main thread; subprocess: handler
  exits within 2s when invoked with a blocked worker).
- tests/test_io_pool.py: module docstring updated to explain the
  reverted atexit approach and point readers at the production fix.

Best-effort: signal.signal may fail on non-main threads (some conftest
warmup paths); failure is swallowed. The conftest's own atexit fix at
8957c9a5 covers the test fixture's normal-exit path.
This commit is contained in:
2026-06-07 02:00:56 -04:00
parent aa70653065
commit abc333f91b
3 changed files with 178 additions and 1 deletions
+10 -1
View File
@@ -1,4 +1,13 @@
"""Tests for src/io_pool.py (the shared 4-thread job pool on AppController)."""
"""Tests for src/io_pool.py (the shared 4-thread job pool on AppController).
Historical note: an earlier revision of this file added two regression
tests asserting that ``make_io_pool`` registered an atexit shutdown
handler. Those tests were reverted together with the production atexit
fix they guarded, because the atexit approach does not solve the actual
Ctrl+C hang (see ``src/io_pool.py`` module docstring). The production
fix is a SIGINT handler in ``AppController.__init__``; the regression
test for that lives in ``tests/test_app_controller_sigint.py``.
"""
import threading
import time