fix(web/api): scope DB-retry sleep so tests don't starve background tasks
test_lifespan_db_retry patched decnet.web.api.asyncio.sleep to skip the DB-retry backoff. Problem: asyncio is a shared module — the patch leaks to every caller that looked up asyncio.sleep via `import asyncio`, including run_health_heartbeat's own sleep loop. That heartbeat task spawns inside the same lifespan; with its sleep mocked, the while-loop spins tight, starves cancellation, and leaves an orphan task that pytest-timeout eventually signals — surfacing as the 'Task exception was never retrieved' warnings the user saw when running the suite. Fix: give decnet.web.api a local binding `_retry_sleep = asyncio.sleep` for the DB-retry wait, and have the test patch that instead. Narrowly scoped, no impact on asyncio.sleep callers elsewhere. Test timing before: 12s with --timeout=10 (interrupted by signal). Test timing after: 0.58s. Full tests/web slice: 27s → 7.1s with the spurious warnings gone.
This commit is contained in:
@@ -1,4 +1,8 @@
|
||||
import asyncio
|
||||
# Local binding for the DB-retry sleep so tests can patch it without
|
||||
# affecting `asyncio.sleep` globally (which would otherwise starve the
|
||||
# heartbeat / worker loops that share the interpreter's asyncio module).
|
||||
from asyncio import sleep as _retry_sleep
|
||||
import os
|
||||
import traceback
|
||||
import uuid
|
||||
@@ -75,7 +79,7 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
|
||||
log.warning("DB init attempt %d/5 failed: %s", attempt, exc)
|
||||
if attempt == 5:
|
||||
log.error("DB failed to initialize after 5 attempts — startup may be degraded")
|
||||
await asyncio.sleep(0.5)
|
||||
await _retry_sleep(0.5)
|
||||
|
||||
# Conditionally enable OpenTelemetry tracing
|
||||
from decnet.telemetry import setup_tracing
|
||||
|
||||
@@ -148,7 +148,10 @@ class TestLifespan:
|
||||
mock_repo.initialize = _failing_init
|
||||
|
||||
with patch("decnet.web.api.repo", mock_repo):
|
||||
with patch("decnet.web.api.asyncio.sleep", new_callable=AsyncMock):
|
||||
# Patch only the local _retry_sleep binding — patching
|
||||
# `asyncio.sleep` globally would starve the heartbeat loop's
|
||||
# own sleep and leak the task past the test's lifetime.
|
||||
with patch("decnet.web.api._retry_sleep", new_callable=AsyncMock):
|
||||
with patch("decnet.web.api.log_ingestion_worker", return_value=asyncio.sleep(0)):
|
||||
with patch("decnet.web.api.log_collector_worker", return_value=asyncio.sleep(0)):
|
||||
with patch("decnet.web.api.attacker_profile_worker", return_value=asyncio.sleep(0)):
|
||||
|
||||
Reference in New Issue
Block a user