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 <mxjerrett@gmail.com>
This commit is contained in:
Rocker Zhang 2026-06-23 20:47:51 +08:00 committed by GitHub
parent 3ccdad6c67
commit 38f1404432
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 37 additions and 2 deletions

View file

@ -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)

View file

@ -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