From 38f1404432984915924f74997d886b89c420b2a8 Mon Sep 17 00:00:00 2001 From: Rocker Zhang Date: Tue, 23 Jun 2026 20:47:51 +0800 Subject: [PATCH] fix(cli): fall back gracefully when embedding-server sidecar is absent (#1206) ## Description `headroom proxy --embedding-server` crashes at startup with `ModuleNotFoundError: No module named 'headroom.memory.adapters.watchdog'` instead of falling back to the per-worker embedder. The `EmbeddingServerWatchdog` import sits above the `try/except` that is meant to catch sidecar-startup failures, so a missing sidecar module raises before the guard runs and takes the whole proxy down. The sidecar module is not present on main (it ships with the dedicated embedding-server sidecar work). ## Type of Change - [x] Bug fix (non-breaking change that fixes an issue) - [ ] New feature (non-breaking change that adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation update - [ ] Performance improvement - [ ] Code refactoring (no functional changes) ## Changes Made - Move the `EmbeddingServerWatchdog` import into the guarded `_start_embed_watchdog` coroutine in `headroom/cli/proxy.py`, so a missing sidecar module is caught by the existing `try/except` and the proxy degrades to the per-worker embedder. - Add `tests/test_cli_proxy_embedding_server.py`, a regression test that forces the sidecar module unimportable and asserts the flag falls back instead of crashing. ## Testing - [ ] Unit tests pass (`pytest`) - [x] Linting passes (`ruff check .`) - [ ] Type checking passes (`mypy headroom`) - [x] New tests added for new functionality - [x] Manual testing performed ### Test Output ```text # The new regression test was validated fail-before / pass-after against the released # build via click's CliRunner (forces the sidecar module unimportable, stubs run_server): # before fix (parent commit): exit_code 1, ModuleNotFoundError, no fallback message # after fix: exit_code 0, no exception, "Falling back to per-worker embedder" # ruff check . and ruff format --check . pass locally on the rebased branch. # Full pytest suite / mypy not run locally; left to CI. ``` ## Real Behavior Proof - Environment: released build (headroom 0.26.0), Linux - Exact command / steps: `headroom proxy --embedding-server --port 8799` - Observed result: the proxy no longer crashes. Before the fix it exits immediately with `ModuleNotFoundError: No module named 'headroom.memory.adapters.watchdog'`; after the fix it logs `WARNING: Failed to start embedding server sidecar: No module named 'headroom.memory.adapters.watchdog'. Falling back to per-worker embedder.`, then prints `URL: http://127.0.0.1:8799` and `Optimization: ENABLED` and serves normally. - Not tested: full pytest suite and mypy locally (left to CI) ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review ## Checklist - [x] My code follows the project's style guidelines - [x] I have performed a self-review of my code - [x] I have commented my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [ ] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [ ] New and existing unit tests pass locally with my changes - [ ] I have updated the CHANGELOG.md if applicable --------- Co-authored-by: JerrettDavis --- headroom/cli/proxy.py | 8 ++++-- tests/test_cli_proxy_embedding_server.py | 31 ++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 2 deletions(-) create mode 100644 tests/test_cli_proxy_embedding_server.py diff --git a/headroom/cli/proxy.py b/headroom/cli/proxy.py index 1ab492acc..1bb88adaf 100644 --- a/headroom/cli/proxy.py +++ b/headroom/cli/proxy.py @@ -1273,9 +1273,13 @@ Press Ctrl+C to stop. import asyncio as _asyncio - from headroom.memory.adapters.watchdog import EmbeddingServerWatchdog - async def _start_embed_watchdog() -> Any: + # Import lazily inside the guarded coroutine. The sidecar module is + # optional and may be absent; keeping the import here lets the + # try/except below fall back to the per-worker embedder instead of + # crashing the proxy at startup with ModuleNotFoundError. + from headroom.memory.adapters.watchdog import EmbeddingServerWatchdog + wd = EmbeddingServerWatchdog(socket_path=_embed_socket) await wd.start() ok = await wd.wait_until_healthy(timeout=30.0) diff --git a/tests/test_cli_proxy_embedding_server.py b/tests/test_cli_proxy_embedding_server.py new file mode 100644 index 000000000..df849a850 --- /dev/null +++ b/tests/test_cli_proxy_embedding_server.py @@ -0,0 +1,31 @@ +"""Regression test for `proxy --embedding-server` startup fallback. + +The optional embedding-server sidecar module +(`headroom.memory.adapters.watchdog`) is not present on main, yet the +`--embedding-server` flag advertises a graceful fallback to the per-worker +embedder. A misplaced import made the flag raise ``ModuleNotFoundError`` at +startup and crash the proxy instead of falling back. +""" + +import sys + +from click.testing import CliRunner + +from headroom.cli import main + + +def test_embedding_server_missing_sidecar_falls_back(monkeypatch): + # Make the optional sidecar module unimportable regardless of whether it is + # installed, so the fallback path is exercised deterministically. + monkeypatch.setitem(sys.modules, "headroom.memory.adapters.watchdog", None) + + # Don't actually start a server. + import headroom.proxy.server as server_mod + + monkeypatch.setattr(server_mod, "run_server", lambda *args, **kwargs: None) + + result = CliRunner().invoke(main, ["proxy", "--embedding-server", "--port", "8799"]) + + assert result.exit_code == 0, f"proxy crashed instead of falling back: {result.output}" + assert result.exception is None + assert "Falling back to per-worker embedder" in result.output