headroom/tests/test_proxy_eager_preload_bind.py
AxelRay d50cfabedc
fix(proxy): report deferred Kompress status and promote health from cache (#2564)
## Description

When Kompress preload is deferred until first request, startup still
logs "not installed" even if ML deps are present. After the model later
loads into the module cache, /readyz and /health can keep reporting
kompress as unhealthy because reconcile only inspected attached
compressor instances. This PR reports deferred startup accurately and
promotes health from the live module cache once the model is ready,
without starting loads from health checks.

Closes #2560

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

- Treat eager-status `deferred` as installed-but-deferred at proxy
startup and log that state instead of "not installed".
- Promote `/readyz` and `/health` Kompress readiness from the
module-level model cache when attached compressors are missing or not
ready.
- Keep health inspection free of lazy getters and download side effects.
- Add regressions for deferred startup logging and cache-based health
promotion.

## Testing

- [x] Unit tests pass (`pytest`)
- [x] Linting passes (`ruff check .`)
- [ ] Type checking passes (`mypy headroom`)
- [x] New tests added for new functionality
- [ ] Manual testing performed

### Test Output

```text
$ PYTHONPATH=/tmp/headroom-2561 python -m pytest tests/test_proxy_health.py tests/test_proxy_eager_preload_bind.py -q -o addopts=
21 passed, 1 warning in 2.73s

$ ruff format --check headroom/proxy/server.py tests/test_proxy_health.py tests/test_proxy_eager_preload_bind.py
3 files already formatted

$ ruff check headroom/proxy/server.py tests/test_proxy_health.py tests/test_proxy_eager_preload_bind.py
All checks passed!
```

## Real Behavior Proof

- Environment: Linux VPS, Python 3.11 venv with headroom-ai 0.32.1 wheel
for `_core`, checked out main + this branch overlayed for source under
test
- Exact command / steps: `PYTHONPATH=/tmp/headroom-2561 python -m pytest
tests/test_proxy_health.py tests/test_proxy_eager_preload_bind.py -q -o
addopts=`; `ruff format --check` and `ruff check` on the three changed
files
- Observed result: 21 focused tests passed, including deferred startup
log regression and module-cache health promotion; ruff format/check
clean
- Not tested: live multi-request proxy with real ONNX model download on
this host; install-status follow-up mentioned in the issue comment

## 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
- [ ] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [x] I did **not** edit `CHANGELOG.md` — it is generated by
release-please from my Conventional Commit PR title (a CI guard enforces
this)

## Screenshots (if applicable)

N/A

## Additional Notes

- Scoped to Kompress status reporting only. The separate `headroom
install status` ownership probe in the issue comment is left for a
follow-up.

Co-authored-by: axelray-dev <axelray-dev@users.noreply.github.com>
2026-07-25 19:24:08 -07:00

143 lines
4.8 KiB
Python

"""Startup must bind its port even when eager preload hangs (#790).
``HeadroomProxy.startup()`` runs inside the ASGI lifespan, which completes
*before* uvicorn binds the socket. The eager compressor/parser preload used to
run synchronously there, so a hang or an uncatchable native stall during a model
load (observed on Windows) left the proxy "never opening its port". The preload
now runs off the event loop under ``asyncio.wait_for`` with
``EAGER_PRELOAD_TIMEOUT_SECONDS``; on timeout startup logs and continues so the
bind still happens and transforms fall back to lazy loading.
"""
from __future__ import annotations
import logging
import threading
import time
import pytest
pytest.importorskip("fastapi")
import headroom.proxy.server as server_mod
from headroom.proxy.server import ProxyConfig, create_app
def _make_proxy(*, optimize: bool):
config = ProxyConfig(
optimize=optimize,
cache_enabled=False,
rate_limit_enabled=False,
cost_tracking_enabled=False,
log_requests=False,
ccr_inject_tool=False,
ccr_handle_responses=False,
ccr_context_tracking=False,
image_optimize=False,
subscription_tracking_enabled=False,
)
return create_app(config).state.proxy
class _FastTransform:
def __init__(self, status):
self._status = status
def eager_load_compressors(self):
return self._status
class _RaisingTransform:
def eager_load_compressors(self):
raise RuntimeError("boom")
class _NonDictTransform:
def eager_load_compressors(self):
return "not-a-dict"
class _HangingTransform:
"""Simulates a model load that hangs forever (released via the event)."""
def __init__(self, release: threading.Event):
self._release = release
def eager_load_compressors(self):
# Safety cap so a misbehaving test can never wedge the suite.
self._release.wait(timeout=30)
return {"hang": "done"}
class _FakePipeline:
def __init__(self, transforms):
self.transforms = transforms
def test_eager_preload_dedupes_and_swallows_failures():
proxy = _make_proxy(optimize=False)
shared = _FastTransform({"shared": "enabled"})
proxy.anthropic_pipeline = _FakePipeline([shared, _FastTransform({"kompress": "enabled"})])
# ``shared`` appears in both pipelines and must load exactly once; the
# raising and non-dict transforms must be skipped without aborting.
proxy.openai_pipeline = _FakePipeline([shared, _RaisingTransform(), _NonDictTransform()])
eager_status, statuses = proxy._eager_preload_transforms()
assert eager_status == {"shared": "enabled", "kompress": "enabled"}
assert statuses == [{"shared": "enabled"}, {"kompress": "enabled"}]
async def test_startup_binds_despite_hung_preload(monkeypatch):
monkeypatch.setattr(server_mod, "EAGER_PRELOAD_TIMEOUT_SECONDS", 0.3)
proxy = _make_proxy(optimize=True)
release = threading.Event()
proxy.anthropic_pipeline = _FakePipeline([_HangingTransform(release)])
proxy.openai_pipeline = _FakePipeline([])
try:
start = time.monotonic()
await proxy.startup() # must NOT wait on the hung load
elapsed = time.monotonic() - start
# Returns shortly after the 0.3s preload timeout, far below the 30s hang.
assert elapsed < 10
finally:
release.set()
await proxy.shutdown()
async def test_startup_merges_warmup_for_normal_transforms(monkeypatch):
proxy = _make_proxy(optimize=True)
captured: list[dict] = []
monkeypatch.setattr(proxy.warmup, "merge_transform_status", captured.append)
proxy.anthropic_pipeline = _FakePipeline([_FastTransform({"kompress": "enabled"})])
proxy.openai_pipeline = _FakePipeline([])
try:
await proxy.startup()
assert {"kompress": "enabled"} in captured
assert proxy._kompress_status == "enabled"
finally:
await proxy.shutdown()
async def test_startup_reports_deferred_kompress(caplog):
proxy = _make_proxy(optimize=True)
proxy.anthropic_pipeline = _FakePipeline([_FastTransform({"kompress": "deferred"})])
proxy.openai_pipeline = _FakePipeline([])
try:
# Proxy setup disables propagation on the ``headroom`` logger, so
# attach caplog's handler directly to the logger that emits this line.
server_mod.logger.addHandler(caplog.handler)
try:
with caplog.at_level(logging.INFO, logger=server_mod.logger.name):
await proxy.startup()
finally:
server_mod.logger.removeHandler(caplog.handler)
assert proxy._kompress_status == "deferred"
assert "Kompress: DEFERRED (model loads on first request)" in caplog.messages
assert not any("Kompress: not installed" in message for message in caplog.messages)
finally:
await proxy.shutdown()