mirror of
https://github.com/usestrix/strix.git
synced 2026-09-08 22:21:05 +00:00
fix(warmup): pre-import the agents SDK to avoid a thread-race crash
The background import warm-up thread imports strix.core.runner (and thus the
openai-agents SDK) while, on the scan path, the main thread independently
imports the same package via strix.report -> strix.report.dedupe ->
`from agents.models.interface import ModelTracing`.
openai-agents has an internal import cycle (agents.agent_output <->
agents.agent). A single thread resolves it, but when two threads import the
`agents` package concurrently, CPython's per-module import locks +
deadlock-avoidance can hand one thread a partially initialized module, crashing
the run with:
ImportError: cannot import name 'AgentOutputSchemaBase' from partially
initialized module 'agents.agent_output' (most likely due to a circular
import)
--version/--help never hit it because they don't import agents; it only shows
up on a real scan, which is why it looked intermittent.
Import the `agents` package once, synchronously, on the caller's thread before
the daemon warm-up thread starts, so it is fully initialized before any
concurrent import. Also start the warm-up after parse_arguments() so
--version/--help/argparse errors exit before the now-partly-synchronous
warm-up runs; it still overlaps the Docker checks and image pull that follow.
Adds tests/test_warmup.py asserting start_import_warmup() leaves `agents` in
sys.modules synchronously.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
bfaaa904f2
commit
47d1bf9ed8
3 changed files with 73 additions and 2 deletions
|
|
@ -433,10 +433,13 @@ def main() -> None:
|
|||
|
||||
from strix.llm.warmup import start_import_warmup
|
||||
|
||||
start_import_warmup()
|
||||
|
||||
# parse_arguments() first so --version/--help/argparse errors exit before
|
||||
# the (heavier) import warm-up runs. Warm-up then overlaps with the I/O
|
||||
# startup below (update check, Docker checks, image pull).
|
||||
args = parse_arguments()
|
||||
|
||||
start_import_warmup()
|
||||
|
||||
start_background_check()
|
||||
if not args.non_interactive and prompt_update_if_available(Console()):
|
||||
if is_binary_install() and sys.platform != "win32":
|
||||
|
|
|
|||
|
|
@ -38,6 +38,24 @@ def _warm(modules: tuple[str, ...]) -> None:
|
|||
logger.debug("Import warm-up for %r failed", name, exc_info=True)
|
||||
|
||||
|
||||
def _preimport_thread_unsafe_sdk() -> None:
|
||||
"""Import the ``agents`` SDK once, on the caller's thread, before the daemon
|
||||
warm-up starts.
|
||||
|
||||
``openai-agents`` has an internal import cycle (``agents.agent_output`` <->
|
||||
``agents.agent``) that is not thread-safe: when two threads import the
|
||||
``agents`` package concurrently, CPython's deadlock-avoidance can hand one
|
||||
of them a partially initialized module, raising ``ImportError: cannot
|
||||
import name 'AgentOutputSchemaBase' ... (circular import)``. Fully importing
|
||||
the package single-threaded here closes that race window before the daemon
|
||||
warm-up thread (and the main thread's later report import) touch it.
|
||||
"""
|
||||
try:
|
||||
importlib.import_module("agents")
|
||||
except Exception: # noqa: BLE001 - a failed warm-up must never fail the run.
|
||||
logger.debug("Pre-import of agents SDK failed", exc_info=True)
|
||||
|
||||
|
||||
def start_import_warmup(modules: tuple[str, ...] = WARMUP_MODULES) -> threading.Thread:
|
||||
"""Start importing the heavy scan dependencies in the background, once.
|
||||
|
||||
|
|
@ -48,6 +66,7 @@ def start_import_warmup(modules: tuple[str, ...] = WARMUP_MODULES) -> threading.
|
|||
with _lock:
|
||||
if _thread is not None:
|
||||
return _thread
|
||||
_preimport_thread_unsafe_sdk()
|
||||
_thread = threading.Thread(
|
||||
target=_warm, args=(modules,), name="strix-import-warmup", daemon=True
|
||||
)
|
||||
|
|
|
|||
49
tests/test_warmup.py
Normal file
49
tests/test_warmup.py
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
import textwrap
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def test_start_import_warmup_preimports_agents_sdk_synchronously() -> None:
|
||||
"""``start_import_warmup`` must import the thread-unsafe ``agents`` SDK on
|
||||
the caller's thread before spawning the daemon warm-up thread.
|
||||
|
||||
``openai-agents`` has an internal import cycle that is not thread-safe: if
|
||||
the warm-up thread and the main thread import the ``agents`` package
|
||||
concurrently, CPython can hand one of them a partially initialized module
|
||||
(``ImportError: cannot import name 'AgentOutputSchemaBase' ... circular
|
||||
import``). Warming ``agents`` synchronously closes that race window.
|
||||
|
||||
Run in a fresh interpreter with an empty ``modules`` set so the daemon
|
||||
thread warms nothing: ``agents`` can then only be in ``sys.modules`` because
|
||||
the synchronous pre-import ran.
|
||||
"""
|
||||
child = textwrap.dedent(
|
||||
"""
|
||||
import sys
|
||||
|
||||
assert "agents" not in sys.modules
|
||||
from strix.llm.warmup import start_import_warmup
|
||||
|
||||
# Importing the warm-up module alone must not pull the agents SDK.
|
||||
assert "agents" not in sys.modules
|
||||
start_import_warmup(modules=())
|
||||
assert "agents" in sys.modules, "agents SDK was not pre-imported synchronously"
|
||||
print("OK")
|
||||
"""
|
||||
)
|
||||
result = subprocess.run( # noqa: S603
|
||||
[sys.executable, "-c", child],
|
||||
cwd=PROJECT_ROOT,
|
||||
check=False,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
|
||||
assert result.returncode == 0, result.stdout + result.stderr
|
||||
assert "OK" in result.stdout
|
||||
Loading…
Add table
Reference in a new issue