From 5edcaf21e6839b44f55597b31f58b13d3ce98c5f Mon Sep 17 00:00:00 2001 From: atkaridarshan04 Date: Thu, 21 May 2026 17:31:22 +0530 Subject: [PATCH 1/2] fix: fail loudly on misconfigured DATABASE_URL or REDIS_URL at startup When DATABASE_URL or REDIS_URL is explicitly set but the target service is unreachable, the app now logs a clear ERROR and raises RuntimeError instead of silently falling back to SQLite / in-process async. - deps.init_job_store: raise instead of SQLite fallback when DATABASE_URL set - lifespan: raise instead of pass when REDIS_URL set but Redis unreachable - worker.startup: explicit error log before re-raise for Postgres failure - docs: note hard startup failure on misconfigured URLs --- app/adapters/http/app.py | 14 ++++++++------ app/adapters/http/deps.py | 11 ++++------- app/infra/queue/worker.py | 9 ++++++++- docs/guides/production-deployment.md | 2 ++ 4 files changed, 22 insertions(+), 14 deletions(-) diff --git a/app/adapters/http/app.py b/app/adapters/http/app.py index cb996d4..592b19d 100644 --- a/app/adapters/http/app.py +++ b/app/adapters/http/app.py @@ -41,12 +41,14 @@ async def lifespan(app: FastAPI): redis_url = os.environ.get("REDIS_URL", "").strip() if redis_url: try: - from app.infra.queue.queue import create_queue - queue = await create_queue(redis_url) - if queue is not None: - _deps.get_async_service()._queue = queue - except Exception: - pass # arq not installed or Redis unreachable — fall back silently + from arq import create_pool + from arq.connections import RedisSettings + pool = await create_pool(RedisSettings.from_dsn(redis_url)) + from app.infra.queue.queue import ArqJobQueue + _deps.get_async_service()._queue = ArqJobQueue(pool) + except Exception as exc: + logger.error("REDIS_URL is set but Redis is unreachable: %s", exc) + raise RuntimeError(f"REDIS_URL is set but Redis is unreachable: {exc}") from exc yield diff --git a/app/adapters/http/deps.py b/app/adapters/http/deps.py index 8703fe6..5897843 100644 --- a/app/adapters/http/deps.py +++ b/app/adapters/http/deps.py @@ -30,16 +30,13 @@ async def init_job_store() -> JobStore: db_url = os.environ.get("DATABASE_URL", "").strip() if db_url: + from app.infra.jobs.postgres_job_store import PostgresJobStore try: - from app.infra.jobs.postgres_job_store import PostgresJobStore _job_store = await PostgresJobStore.create_pool(dsn=db_url) - return _job_store except Exception as exc: - _log.error( - "Failed to connect to Postgres (%s). " - "Falling back to SQLite — THIS IS NOT SAFE IN PRODUCTION.", - exc, - ) + _log.error("DATABASE_URL is set but Postgres is unreachable: %s", exc) + raise RuntimeError(f"DATABASE_URL is set but Postgres is unreachable: {exc}") from exc + return _job_store from app.infra.jobs.sqlite_job_store import SQLiteJobStore _job_store = SQLiteJobStore() diff --git a/app/infra/queue/worker.py b/app/infra/queue/worker.py index 3b56253..2fc2324 100644 --- a/app/infra/queue/worker.py +++ b/app/infra/queue/worker.py @@ -4,9 +4,12 @@ Run the worker: arq app.infra.queue.worker.WorkerSettings """ +import logging import os from typing import Any +logger = logging.getLogger(__name__) + async def run_inference(ctx: dict, job_id: str, model: str, version: str, payload: Any) -> None: from uuid import UUID @@ -61,7 +64,11 @@ async def startup(ctx: dict) -> None: db_url = os.environ.get("DATABASE_URL", "").strip() if db_url: from app.infra.jobs.postgres_job_store import PostgresJobStore - store = await PostgresJobStore.create_pool(dsn=db_url) + try: + store = await PostgresJobStore.create_pool(dsn=db_url) + except Exception as exc: + logger.error("DATABASE_URL is set but Postgres is unreachable: %s", exc) + raise RuntimeError(f"DATABASE_URL is set but Postgres is unreachable: {exc}") from exc else: from app.infra.jobs.sqlite_job_store import SQLiteJobStore store = SQLiteJobStore() diff --git a/docs/guides/production-deployment.md b/docs/guides/production-deployment.md index 2e6c931..b2c3f2b 100644 --- a/docs/guides/production-deployment.md +++ b/docs/guides/production-deployment.md @@ -19,6 +19,8 @@ REDIS_URL=redis://localhost:6379/0 When `ENV=production` and `API_KEYS` is not set, the server refuses to start. +When `DATABASE_URL` or `REDIS_URL` is set but the service is unreachable, the server (and arq worker) refuse to start with a clear error instead of silently falling back to SQLite or in-process async. Ensure both services are healthy before starting the application. + --- ## Start services From b629678eca60e514217cf03c785918bdb0479e74 Mon Sep 17 00:00:00 2001 From: atkaridarshan04 Date: Thu, 21 May 2026 17:37:23 +0530 Subject: [PATCH 2/2] fix: update test to expect RuntimeError instead of SQLite fallback test_falls_back_to_sqlite_when_postgres_unavailable renamed to test_raises_when_database_url_set_but_postgres_unavailable and updated to assert RuntimeError is raised, matching the new hard-failure behaviour. --- tests/test_engine_phase2.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/tests/test_engine_phase2.py b/tests/test_engine_phase2.py index e681a80..0768855 100644 --- a/tests/test_engine_phase2.py +++ b/tests/test_engine_phase2.py @@ -660,8 +660,7 @@ async def fake_create_pool(dsn=None): store = asyncio.run(deps_mod.init_job_store()) assert store is mock_store - def test_falls_back_to_sqlite_when_postgres_unavailable(self): - from app.infra.jobs.sqlite_job_store import SQLiteJobStore + def test_raises_when_database_url_set_but_postgres_unavailable(self): import app.adapters.http.deps as deps_mod async def fail(*a, **kw): @@ -672,5 +671,5 @@ async def fail(*a, **kw): "app.infra.jobs.postgres_job_store.PostgresJobStore.create_pool", side_effect=fail, ): - store = asyncio.run(deps_mod.init_job_store()) - assert isinstance(store, SQLiteJobStore) + with pytest.raises(RuntimeError, match="DATABASE_URL is set but Postgres is unreachable"): + asyncio.run(deps_mod.init_job_store())