mirror of
https://github.com/headroomlabs-ai/headroom.git
synced 2026-08-27 14:17:10 -04:00
fix(proxy): reset memory backend to None on init timeout
If asyncio.wait_for fires while LocalBackend._ensure_initialized() is running, _init_backend_locked may have already assigned self._backend (the LocalBackend constructor) before its own await raised or was cancelled. Callers doing if self.memory_handler._backend: then see a truthy-but-broken backend. Null self._backend in the TimeoutError handler so the post-timeout state is consistent: _initialized=False AND _backend=None.
This commit is contained in:
parent
0e166c60d4
commit
3f1f82d7e2
2 changed files with 47 additions and 0 deletions
|
|
@ -185,6 +185,12 @@ class MemoryHandler:
|
|||
await asyncio.wait_for(_do_init(), timeout=STARTUP_INIT_TIMEOUT_SECONDS)
|
||||
except asyncio.TimeoutError:
|
||||
# Fail-open: leave _initialized=False so subsequent calls retry.
|
||||
# CRITICAL: also null the backend — _init_backend_locked may have
|
||||
# already assigned ``self._backend`` before its own await raised /
|
||||
# was cancelled by wait_for. Callers that do
|
||||
# ``if self.memory_handler._backend:`` must not see a
|
||||
# truthy-but-broken backend.
|
||||
self._backend = None
|
||||
logger.error(
|
||||
"Memory: backend initialization timed out after "
|
||||
f"{STARTUP_INIT_TIMEOUT_SECONDS}s "
|
||||
|
|
|
|||
|
|
@ -149,6 +149,47 @@ async def test_ensure_initialized_timeout_leaves_handler_unready(
|
|||
assert STARTUP_INIT_TIMEOUT_SECONDS == 30.0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ensure_initialized_timeout_nulls_partially_initialized_backend(
|
||||
tmp_path, monkeypatch
|
||||
):
|
||||
"""If wait_for fires while _init_backend_locked has already set
|
||||
``self._backend`` but before ``self._initialized = True``, the timeout
|
||||
handler must null ``_backend``. Otherwise callers doing
|
||||
``if self.memory_handler._backend:`` see a truthy-but-broken backend.
|
||||
"""
|
||||
|
||||
class SlowBackend:
|
||||
def __init__(self, config):
|
||||
self.config = config
|
||||
|
||||
async def _ensure_initialized(self) -> None:
|
||||
# Hang long enough to blow the 0.01s timeout below.
|
||||
await asyncio.sleep(5.0)
|
||||
|
||||
async def close(self) -> None:
|
||||
pass
|
||||
|
||||
import headroom.memory.backends.local as local_mod
|
||||
|
||||
monkeypatch.setattr(local_mod, "LocalBackend", SlowBackend)
|
||||
|
||||
handler = MemoryHandler(
|
||||
MemoryConfig(
|
||||
enabled=True, backend="local", db_path=str(tmp_path / "mem.db")
|
||||
)
|
||||
)
|
||||
|
||||
with patch(
|
||||
"headroom.proxy.memory_handler.STARTUP_INIT_TIMEOUT_SECONDS", 0.01
|
||||
):
|
||||
await handler._ensure_initialized()
|
||||
|
||||
# Both must be consistent after timeout.
|
||||
assert handler._initialized is False
|
||||
assert handler._backend is None
|
||||
|
||||
|
||||
# -------------------------------------------------------------------
|
||||
# Real backend init (no monkeypatching) — integration smoke test
|
||||
# -------------------------------------------------------------------
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue