diff --git a/AGENTS.md b/AGENTS.md index 1cc84f9..b2dde28 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -36,7 +36,7 @@ Python is 3.14, dependencies managed by `uv`. The API is exposed on `:8000`. `UseCases` group (request-scoped): one provider per use case in `app/use_cases/`. modern-di fills each dataclass field by its type, so a use case needs no `kwargs`. -**Pool and server sizing**: `DB_POOL_SIZE` (positive) and `DB_MAX_OVERFLOW` (non-negative) size the pool, and a request that finds it empty waits for a connection. Granian's `backpressure` is left at its default: it limits open connections per worker, idle keep-alive ones included, so setting it to the pool size makes new connections wait while the app is idle. `APP_WORKERS` sets the Granian worker count, and each worker has its own pool. `DB_POOL_TIMEOUT` (default 30 s) bounds the wait for a connection, and a timeout returns 503 through `exceptions.database_unavailable_handler`. advanced-alchemy wraps the timeout in a `RepositoryError`, so the handler is also registered for `RepositoryError` and answers anything not caused by a pool timeout the way Litestar's default handler would: the debug page when `SERVICE_DEBUG` is on, a plain 500 otherwise. `DB_POOL_RECYCLE` defaults to SQLAlchemy's `-1` (never). Set it below the idle timeout of any proxy or load balancer in front of Postgres, so a connection it dropped is not handed out and retried after a 1-2 s backoff. +**Pool and server sizing**: `DB_POOL_SIZE` (positive) and `DB_MAX_OVERFLOW` (non-negative) size the pool, and a request that finds it empty waits for a connection. Granian's `backpressure` is left at its default: it limits open connections per worker, idle keep-alive ones included, so setting it to the pool size makes new connections wait while the app is idle. `APP_WORKERS` sets the Granian worker count, and each worker has its own pool. `DB_POOL_TIMEOUT` (default 5 s) bounds the wait for a connection, and a timeout returns 503 through `exceptions.database_unavailable_handler`. advanced-alchemy wraps the timeout in a `RepositoryError`, so the handler is also registered for `RepositoryError` and answers anything not caused by a pool timeout the way Litestar's default handler would: the debug page when `SERVICE_DEBUG` is on, a plain 500 otherwise. `DB_POOL_RECYCLE` defaults to SQLAlchemy's `-1` (never). Set it below the idle timeout of any proxy or load balancer in front of Postgres, so a connection it dropped is not handed out and retried after a 1-2 s backoff. **Use cases and lost connections**: `pool_pre_ping` is off by default (`DB_POOL_PRE_PING`). A pooled connection that died while idle is handled by `db_retry.postgres_retry` instead, which saves a round trip on every checkout. That only works if the retried function owns its whole transaction, so every use case has this shape: diff --git a/app/settings.py b/app/settings.py index 373fb30..58f5fd9 100644 --- a/app/settings.py +++ b/app/settings.py @@ -19,7 +19,7 @@ class Settings(pydantic_settings.BaseSettings): db_pool_size: pydantic.PositiveInt = 5 db_max_overflow: pydantic.NonNegativeInt = 0 db_pool_pre_ping: bool = False - db_pool_timeout: float = 30 + db_pool_timeout: float = 5 db_pool_recycle: int = -1 app_host: str = "0.0.0.0" # noqa: S104