This commit is contained in:
hasht 2026-08-27 17:19:48 -04:00 committed by GitHub
commit b297bba4c4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 44 additions and 24 deletions

View file

@ -393,6 +393,15 @@ def _bootstrap_scan(args: argparse.Namespace) -> None:
preflight and run preparation happen inside the TUI so the interface
paints immediately instead of waiting on a model round trip.
"""
# Pre-import the heavy scan dependencies once, on the confirmed scan path.
# This is deliberately not done before argument parsing: warm-up is
# synchronous (see strix.llm.warmup), so running it earlier would make cold
# --help/--version/--update and interactive setup wait on the full scan
# import graph for no benefit.
from strix.llm.warmup import start_import_warmup
start_import_warmup()
validate_environment()
if not args.non_interactive:
return
@ -431,10 +440,6 @@ def main() -> None:
sys.exit(run_auth(sys.argv[2:]))
from strix.llm.warmup import start_import_warmup
start_import_warmup()
args = parse_arguments()
start_background_check()

View file

@ -1,13 +1,28 @@
"""Background pre-import of the heavy scan dependencies.
"""Pre-import of the heavy scan dependencies.
The scan engine's import graph (the agents SDK, OpenAI client, LiteLLM, the
Caido SDK, the Docker SDK) costs seconds to import cold, but none of it is
needed until a scan actually starts. Importing it on a daemon thread at CLI
entry overlaps that cost with the I/O-bound startup work that always precedes
a scan (argument parsing, Docker checks, image pull, TUI setup), so by the
time the scan begins the modules are already in ``sys.modules``. Any thread
that needs one of them before the warm-up finishes just blocks on the normal
import lock, so behaviour is unchanged either way.
needed until a scan actually starts. We import it here, once, at the start of
the scan bootstrap so the cost is paid up front rather than on first use deep
in the run.
This used to run on a background daemon thread to overlap the import cost with
the I/O-bound startup work that precedes a scan. That is unsound: the warm-up
imports ``strix.core.runner``, which pulls in the agents SDK, whose package
graph has internal circular imports. CPython resolves circular imports by
returning a *partially initialized* module to break the cycle, and that partial
state is observable from other threads. When the warm-up thread and the main
thread (``warm_up_llm`` imports ``agents.model_settings`` /
``agents.models.interface``) import that graph concurrently, they intermittently
observe each other's partial ``agents`` package and crash with
``KeyError: 'agents'`` or "cannot import name ... from partially initialized
module 'agents.agent_output'". This reproduces reliably on some hosts (e.g.
WSL2). The per-module import lock does not prevent it precisely because the
import system deliberately hands out partial modules mid-cycle.
Warm-up only runs on the scan path, which eagerly imports all of these modules
anyway, so importing synchronously here adds no net work - it just removes the
racy overlap. See https://github.com/usestrix/strix (import-warmup race).
"""
from __future__ import annotations
@ -27,7 +42,7 @@ WARMUP_MODULES = (
)
_lock = threading.Lock()
_thread: threading.Thread | None = None
_warmed: bool = False
def _warm(modules: tuple[str, ...]) -> None:
@ -38,18 +53,18 @@ def _warm(modules: tuple[str, ...]) -> None:
logger.debug("Import warm-up for %r failed", name, exc_info=True)
def start_import_warmup(modules: tuple[str, ...] = WARMUP_MODULES) -> threading.Thread:
"""Start importing the heavy scan dependencies in the background, once.
def start_import_warmup(modules: tuple[str, ...] = WARMUP_MODULES) -> None:
"""Import the heavy scan dependencies once, synchronously.
``modules`` lets embedders that never touch some backends (e.g. a cloud
runtime that has no local Docker) warm a narrower set.
Runs on the calling thread rather than a background daemon: importing the
agents-SDK-bearing graph concurrently with the main thread races on the
SDK's internal circular imports (see module docstring). ``modules`` lets
embedders that never touch some backends (e.g. a cloud runtime that has no
local Docker) warm a narrower set.
"""
global _thread # noqa: PLW0603
global _warmed # noqa: PLW0603
with _lock:
if _thread is not None:
return _thread
_thread = threading.Thread(
target=_warm, args=(modules,), name="strix-import-warmup", daemon=True
)
_thread.start()
return _thread
if _warmed:
return
_warm(modules)
_warmed = True