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:
@@ -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
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user