diff --git a/decnet/web/api.py b/decnet/web/api.py index a15c63d0..b4689f6c 100644 --- a/decnet/web/api.py +++ b/decnet/web/api.py @@ -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 diff --git a/tests/web/test_web_api.py b/tests/web/test_web_api.py index b07a1f8d..988a9d4f 100644 --- a/tests/web/test_web_api.py +++ b/tests/web/test_web_api.py @@ -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)):