diff --git a/backend/app/main.py b/backend/app/main.py index 9e39f45d..80e22538 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -141,7 +141,7 @@ async def lifespan(_app: FastAPI) -> AsyncIterator[None]: finally: if arq_pool is not None: try: - await arq_pool.close() + await arq_pool.aclose() except Exception as exc: # noqa: BLE001 — shutdown swallow logger.warning("arq pool close raised during shutdown", error=str(exc)) # Cancel the capability check if it's still running on shutdown diff --git a/backend/tests/unit/test_main_lifespan.py b/backend/tests/unit/test_main_lifespan.py index c0b26830..18344f82 100644 --- a/backend/tests/unit/test_main_lifespan.py +++ b/backend/tests/unit/test_main_lifespan.py @@ -83,6 +83,7 @@ def _patched_externals(monkeypatch: pytest.MonkeyPatch) -> dict[str, Any]: import arq.connections fake_pool = MagicMock() + fake_pool.aclose = AsyncMock(return_value=None) fake_pool.close = AsyncMock(return_value=None) monkeypatch.setattr( arq.connections, @@ -167,6 +168,26 @@ async def _fake_warmup(db_factory: Any, redis_client: Any) -> None: # Warmup is gated off — coroutine NEVER called. assert len(warmup_invocations) == 0, warmup_invocations + async def test_lifespan_closes_arq_pool_with_aclose( + self, + monkeypatch: pytest.MonkeyPatch, + _patched_externals: dict[str, Any], + ) -> None: + """Regression guard: Arq pool shutdown uses non-deprecated aclose().""" + monkeypatch.setenv("RELYLOOP_DISABLE_STARTUP_WARMUP", "1") + + async def _fake_cap_noop(**_kwargs: Any) -> None: + return None + + monkeypatch.setattr(app_main, "run_capability_check_background", _fake_cap_noop) + + app = FastAPI() + async with app_main.lifespan(app): + await asyncio.sleep(0) + + _patched_externals["pool"].aclose.assert_awaited_once() + _patched_externals["pool"].close.assert_not_called() + class TestLifespanShutdownCancels: async def test_shutdown_cancels_warmup_task_if_running( diff --git a/backend/tests/unit/test_workers.py b/backend/tests/unit/test_workers.py index e999290d..ad6e6ca8 100644 --- a/backend/tests/unit/test_workers.py +++ b/backend/tests/unit/test_workers.py @@ -14,6 +14,7 @@ import os from pathlib import Path +from unittest.mock import AsyncMock, MagicMock import pytest @@ -111,6 +112,27 @@ def test_worker_settings_has_on_startup_hook(_settings_env: None) -> None: assert callable(WorkerSettings.on_startup) +async def test_on_shutdown_closes_arq_pool_with_aclose(_settings_env: None) -> None: + """on_shutdown awaits ArqRedis.aclose() and avoids deprecated close().""" + from backend.workers.all import on_shutdown + + fake_pool = MagicMock() + fake_pool.aclose = AsyncMock(return_value=None) + fake_pool.close = AsyncMock(return_value=None) + + await on_shutdown({"arq_pool": fake_pool}) + + fake_pool.aclose.assert_awaited_once() + fake_pool.close.assert_not_called() + + +async def test_on_shutdown_without_pool_is_noop(_settings_env: None) -> None: + """A missing Arq pool should not raise during worker shutdown.""" + from backend.workers.all import on_shutdown + + await on_shutdown({}) + + def test_worker_settings_redis_host_parsed(_settings_env: None) -> None: """RedisSettings.from_dsn should pull host=redis port=6379 db=0 from the URL.""" # Re-import so the class-level redis_settings is rebuilt with our env diff --git a/backend/workers/all.py b/backend/workers/all.py index e2fe60b4..b94d3823 100644 --- a/backend/workers/all.py +++ b/backend/workers/all.py @@ -222,7 +222,7 @@ async def on_shutdown(ctx: dict[str, Any]) -> None: arq_pool: ArqRedis | None = ctx.get("arq_pool") if arq_pool is not None: - await arq_pool.close() + await arq_pool.aclose() class WorkerSettings: