Private
Public Access
0
0

fix(conftest): register atexit handler for non-blocking pool shutdown

Fixes the run_tests_batched.py hang that occurs after batch 4.
The original conftest (commit 52ea2693) stored _warmup_app_controller
at module scope for the entire pytest session. When pytest exits,
GC of the AppController triggers ThreadPoolExecutor.__del__ ->
shutdown(wait=True). If warmup hasn't fully completed by then, the
shutdown blocks indefinitely, causing the batched test runner to
hang at the subprocess.run boundary.

Fix: register an atexit handler that captures the _io_pool reference
directly (default argument) and shuts it down with wait=False. The
pool reference is captured by closure, surviving even after the
AppController is GC'd. shutdown() is idempotent so the subsequent
shutdown(wait=True) in __del__ is a no-op.

This is part of sub-track 4 (warmup notification) cleanup; the
conftest's wait_for_warmup behavior is preserved, only the
exit-hang is fixed.
This commit is contained in:
2026-06-06 21:35:05 -04:00
parent f3d071e0c8
commit 8957c9a5be
+20
View File
@@ -33,6 +33,20 @@ install()
# created in a session-scoped fixture; if it already exists (e.g., # created in a session-scoped fixture; if it already exists (e.g.,
# the live_gui fixture also creates one), this call is a no-op or # the live_gui fixture also creates one), this call is a no-op or
# fast (warmup already done). # fast (warmup already done).
#
# FIX (startup_speedup_20260606 sub-track 4 follow-up): The original
# code held `_warmup_app_controller` at module scope for the entire
# pytest session. When pytest exits, GC of the AppController triggers
# ThreadPoolExecutor.__del__ -> shutdown(wait=True). If warmup hasn't
# fully completed, shutdown blocks indefinitely, causing the batched
# test runner to hang after pytest exits.
#
# Fix: register an atexit handler that captures the pool reference
# directly (not the AppController) and shuts it down with wait=False.
# shutdown() is idempotent, so the subsequent shutdown(wait=True) in
# __del__ is a no-op. The pool reference is captured by closure so it
# survives even after the AppController is GC'd.
import atexit
from src.app_controller import AppController from src.app_controller import AppController
_warmup_app_controller = AppController() _warmup_app_controller = AppController()
if not _warmup_app_controller.wait_for_warmup(timeout=60.0): if not _warmup_app_controller.wait_for_warmup(timeout=60.0):
@@ -44,6 +58,12 @@ if not _warmup_app_controller.wait_for_warmup(timeout=60.0):
RuntimeWarning, RuntimeWarning,
stacklevel=2, stacklevel=2,
) )
_warmup_io_pool = getattr(_warmup_app_controller, "_io_pool", None)
def _shutdown_warmup_pool(pool: object = _warmup_io_pool) -> None:
if pool is not None:
try: pool.shutdown(wait=False)
except Exception: pass
atexit.register(_shutdown_warmup_pool)
from src.gui_2 import App from src.gui_2 import App