7a0da7932b
OSV-Scanner (Scheduled) / scan-scheduled (push) Failing after 0s
Create Release / test-gate (push) Has been cancelled
Create Release / release-gate (push) Has been cancelled
Create Release / ci-gate (push) Has been cancelled
Create Release / version-check (push) Has been cancelled
Create Release / e2e-test-gate (push) Has been cancelled
Create Release / responsive-test-gate (push) Has been cancelled
Create Release / compat-test-gate (push) Has been cancelled
Create Release / compose-integration-gate (push) Has been cancelled
Create Release / vulture-gate (push) Has been cancelled
Create Release / build (push) Has been cancelled
Create Release / provenance (push) Has been cancelled
Create Release / prerelease-docker (push) Has been cancelled
Create Release / publish-docker (push) Has been cancelled
Create Release / create-release (push) Has been cancelled
Create Release / cleanup-changelog (push) Has been cancelled
Create Release / trigger-pypi (push) Has been cancelled
Create Release / monitor-pypi (push) Has been cancelled
Create Release / Clean up orphan prerelease tags and signatures (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-form] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-metrics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-workflow] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-core] (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [history-news] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [library] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [link-analytics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-core] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-lifecycle] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [error-benchmark] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) (push) Has been cancelled
Docker Tests (Consolidated) / Accessibility Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Unit Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Example Tests (push) Has been cancelled
Docker Tests (Consolidated) / Production Image Smoke Test (push) Has been cancelled
Docker Tests (Consolidated) / Infrastructure Tests (push) Has been cancelled
OSSF Scorecard / OSSF Security Scorecard Analysis (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [mobile] (push) Has been cancelled
Backwards Compatibility / Verify Encryption Constants (push) Has been cancelled
Backwards Compatibility / PyPI Version Compatibility (push) Has been cancelled
Backwards Compatibility / Database Migration Tests (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Docker Tests (Consolidated) / detect-changes (push) Has been cancelled
Docker Tests (Consolidated) / Build Test Image (push) Has been cancelled
Docker Tests (Consolidated) / All Pytest Tests + Coverage (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [accessibility] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [api-crud] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-login] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-register] (push) Has been cancelled
159 lines
6.9 KiB
Python
159 lines
6.9 KiB
Python
# allow: no-sut-import — guardian; runs create_app()/imports the SUT startup
|
|
# path in a fresh subprocess (in-process imports would mask the regression)
|
|
# and asserts the sentence-transformers / langchain-text-splitters stack is
|
|
# absent from sys.modules.
|
|
"""Regression guard for the app-startup import weight.
|
|
|
|
Background
|
|
----------
|
|
``create_app()`` runs on every app startup. It used to load the
|
|
sentence-transformers / langchain-text-splitters stack (~2s+, and on the
|
|
scheduler path the full transformers/torch stack, ~6-10s) at import time,
|
|
which on slow CI runners pushed startup past the 20s ``faulthandler``
|
|
watchdog (``responsive-test-gate / ui-tests``) and hung the app. The
|
|
original faulthandler dump showed the main thread stuck importing
|
|
``scheduler/background.py`` (it transitively imported the RAG service ->
|
|
``embeddings.splitters`` -> ``langchain_text_splitters`` -> sentence
|
|
transformers). The lazy text-splitter fix landed in #4829; see also the
|
|
earlier #4490 / #4431.
|
|
|
|
What this guard does (and does NOT) assert
|
|
------------------------------------------
|
|
#4829 removed the **sentence-transformers / langchain-text-splitters**
|
|
stack from the startup path (it is now imported lazily, only when text
|
|
splitting / RAG indexing actually runs). That is the invariant guarded
|
|
here.
|
|
|
|
It does NOT assert ``transformers`` / ``torch`` are absent from
|
|
``create_app()``. Those are STILL imported at startup through a separate,
|
|
pre-existing path not addressed by the lazy-splitter fix:
|
|
``web/app_factory.py`` register_blueprints -> most route blueprints ->
|
|
``search_system`` / ``config.llm_config`` ->
|
|
``langchain_core.language_models``, whose ``base`` module imports
|
|
``transformers`` at module load (``from transformers import
|
|
GPT2TokenizerFast`` — inside a ``try/except ImportError``, but transformers
|
|
is installed here, so it fires). ``BaseChatModel`` is imported at module
|
|
level by ~15 core modules and most startup route blueprints, so removing
|
|
transformers/torch from ``create_app()`` is an architectural change well
|
|
beyond the splitter fix. Asserting their absence here would be a guaranteed
|
|
false failure.
|
|
|
|
Both checks run in a FRESH subprocess: the broader pytest session has
|
|
almost always already imported these modules into ``sys.modules``, which
|
|
would mask any regression in-process.
|
|
|
|
If a check fails, some module on the startup path grew a module-level
|
|
import that drags in the splitter stack. Fix it by deferring that import
|
|
to the function where it is used (an RAG / text-splitting job), not by
|
|
relaxing this test.
|
|
"""
|
|
|
|
import subprocess
|
|
import sys
|
|
|
|
import pytest
|
|
|
|
# Probe 1: run the REAL create_app() in a clean interpreter and assert the
|
|
# splitter stack never loads. Covers the ENTIRE startup surface (every
|
|
# blueprint + middleware + queue processor), so there is no "which module did
|
|
# we forget to import" blind spot. Side-effecting singletons are patched out
|
|
# and all state is redirected to a throwaway data dir for determinism.
|
|
_PROBE_CREATE_APP = """
|
|
import sys, os, shutil, tempfile
|
|
_data_dir = tempfile.mkdtemp(prefix="ldr_startup_guard_")
|
|
os.environ["LDR_DATA_DIR"] = _data_dir
|
|
os.environ.setdefault("TESTING", "1")
|
|
from unittest.mock import patch, MagicMock
|
|
|
|
try:
|
|
with (
|
|
patch("local_deep_research.web.app_factory.SocketIOService"),
|
|
patch("local_deep_research.web.queue.processor_v2.queue_processor"),
|
|
patch("atexit.register"),
|
|
patch(
|
|
"local_deep_research.scheduler.background.get_background_job_scheduler",
|
|
return_value=MagicMock(),
|
|
),
|
|
):
|
|
from local_deep_research.web.app_factory import create_app
|
|
create_app()
|
|
|
|
stack = ("sentence_transformers", "langchain_text_splitters")
|
|
loaded = sorted(m for m in stack if m in sys.modules)
|
|
if loaded:
|
|
print("SPLITTER_STACK_IMPORTED:" + ",".join(loaded))
|
|
sys.exit(1)
|
|
print("CREATE_APP_SPLITTER_CLEAN")
|
|
finally:
|
|
# create_app() writes a .secret_key into LDR_DATA_DIR; clean it up so the
|
|
# guard doesn't leak a temp dir per run on persistent dev machines.
|
|
shutil.rmtree(_data_dir, ignore_errors=True)
|
|
"""
|
|
|
|
# Probe 2: the original faulthandler hang site. Importing scheduler.background
|
|
# in isolation must stay fully decoupled from the ML stack — not even
|
|
# transformers/torch at module load. NOTE: #4829 already decoupled the
|
|
# scheduler -> RAG service -> splitter chain, so a regression in
|
|
# text_splitter_registry is caught by Probe 1, not here. This probe guards the
|
|
# scheduler module's OWN import surface — it trips if a NEW heavy import lands
|
|
# directly on scheduler.background's chain (a tighter, non-redundant contract).
|
|
_PROBE_SCHEDULER = """
|
|
import sys
|
|
import local_deep_research.scheduler.background # noqa: F401
|
|
|
|
heavy = ("transformers", "sentence_transformers", "torch", "langchain_text_splitters")
|
|
loaded = sorted(m for m in heavy if m in sys.modules)
|
|
if loaded:
|
|
print("SCHEDULER_IMPORT_HEAVY:" + ",".join(loaded))
|
|
sys.exit(1)
|
|
print("SCHEDULER_IMPORT_CLEAN")
|
|
"""
|
|
|
|
|
|
def _run(probe: str) -> subprocess.CompletedProcess:
|
|
# Keep this comfortably below pyproject's global pytest timeout (180s,
|
|
# thread method). If a probe ever genuinely hangs, we want subprocess.run
|
|
# to raise a clean TimeoutExpired here — yielding a localized failure with
|
|
# the partial output — rather than pytest-timeout firing first and
|
|
# hard-killing the whole xdist worker (losing its other tests/coverage).
|
|
try:
|
|
return subprocess.run(
|
|
[sys.executable, "-c", probe],
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=120,
|
|
)
|
|
except subprocess.TimeoutExpired as exc:
|
|
pytest.fail(
|
|
"Startup-import probe did not finish within 120s — likely a hang on "
|
|
f"the app-startup path.\npartial stdout:\n{exc.stdout}\n"
|
|
f"partial stderr:\n{exc.stderr}"
|
|
)
|
|
|
|
|
|
def test_create_app_does_not_import_splitter_stack():
|
|
"""Running create_app() must not load sentence-transformers / splitters."""
|
|
result = _run(_PROBE_CREATE_APP)
|
|
assert (
|
|
result.returncode == 0 and "CREATE_APP_SPLITTER_CLEAN" in result.stdout
|
|
), (
|
|
"create_app() pulled the sentence-transformers / langchain-text-splitters "
|
|
"stack onto the startup path. This reintroduces the startup-watchdog hang "
|
|
"fixed in #4829 — defer the offending top-level import to its call "
|
|
f"site.\nstdout:\n{result.stdout}\nstderr:\n{result.stderr}"
|
|
)
|
|
|
|
|
|
def test_scheduler_import_is_ml_free():
|
|
"""Importing scheduler.background (the original hang site) loads no ML stack."""
|
|
result = _run(_PROBE_SCHEDULER)
|
|
assert (
|
|
result.returncode == 0 and "SCHEDULER_IMPORT_CLEAN" in result.stdout
|
|
), (
|
|
"Importing local_deep_research.scheduler.background pulled in the ML stack. "
|
|
"Keep the splitter import lazy (inside get_text_splitter) — and any RAG "
|
|
"service import out of module scope — so the scheduler import on the "
|
|
f"startup critical path stays light.\n"
|
|
f"stdout:\n{result.stdout}\nstderr:\n{result.stderr}"
|
|
)
|