fix(storage): address review on PR #799 (stale comments, docs deprecation, unit test)
claude-review on #799 flagged: 1. Stale inline comment in ``initialize()`` (line 466) still said "Postgres uses a small bounded pool". Updated to reflect both backends now use NullPool. 2. Stale ``close()`` docstring referenced pool-size starving max_connections — irrelevant with NullPool. Replaced with the NullPool-aware rationale (dispose still tears down in-flight asyncpg connections cleanly). 3. ``docs/configuration.md`` actively directed operators to tune DATABASE_POOL_SIZE / DATABASE_MAX_OVERFLOW, with worked examples and pool math. Both are now deprecated no-ops; the table entries explain the deprecation and link to PR #799. Operators reading the docs will no longer be confused into tuning settings that don't do anything. 4. ``config.py`` comment for the deprecated fields updated to record the deprecation. Validators are intentionally kept (still reject < 1 / < 0) so misconfigured deploys fail loudly rather than silently — the reviewer flagged this as a minor UX wart but explicitly "not a blocker"; the docs change in (3) keeps operators away from the config altogether. 5. New ``tests/unit/test_storage_engine.py`` with three tests: - ``test_postgres_engine_uses_nullpool`` — pins ``isinstance( engine.pool, NullPool)`` so a refactor back to QueuePool / SingletonThreadPool can't silently re-introduce the cross- event-loop crashes. - ``test_postgres_engine_ignores_pool_sizing_settings`` — setting DATABASE_POOL_SIZE / DATABASE_MAX_OVERFLOW to huge values must not change pool type (proves the deprecated fields are wired-up no-ops). - ``test_postgres_engine_missing_asyncpg_driver_message`` — guards the existing actionable-error branch when the optional ``[postgres]`` extra isn't installed. Verified: - ``uv run pytest tests/unit/`` — 1028 passed - ``uv run ruff check`` clean on the touched python files Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
8cd3092e87
commit
e98903c502
@@ -463,10 +463,11 @@ class RefreshTokenStorage:
|
||||
if Path(self.db_path).exists():
|
||||
os.chmod(self.db_path, 0o600)
|
||||
|
||||
# Create the shared async engine for the chosen backend. SQLite uses
|
||||
# NullPool (per-call connections, matches the prior aiosqlite-direct
|
||||
# behavior); Postgres uses a small bounded pool — see
|
||||
# ``_build_postgres_engine`` for sizing rationale.
|
||||
# Create the shared async engine for the chosen backend. Both
|
||||
# SQLite and Postgres use NullPool (per-call connections, no
|
||||
# cross-loop bookkeeping). SQLite mirrors the prior
|
||||
# aiosqlite-direct behavior; see ``_build_postgres_engine`` for
|
||||
# the Postgres rationale.
|
||||
if is_sqlite:
|
||||
self.engine = create_async_engine(
|
||||
self.database_url,
|
||||
@@ -598,12 +599,11 @@ class RefreshTokenStorage:
|
||||
async def close(self) -> None:
|
||||
"""Dispose the underlying AsyncEngine on shutdown.
|
||||
|
||||
Without an explicit dispose, asyncpg's pooled connections leak
|
||||
server-side slots until the Postgres
|
||||
``idle_in_transaction_session_timeout`` reaps them — with the
|
||||
small pool defaults and frequent k8s rolling restarts this can
|
||||
starve ``max_connections``. Idempotent: safe to call from any
|
||||
number of shutdown hooks.
|
||||
With ``NullPool`` the dispose call has no idle pool to drain,
|
||||
but it still cleanly tears down any in-flight asyncpg
|
||||
connections held by active checkouts so shutdown hooks don't
|
||||
leave dangling transports behind. Idempotent: safe to call
|
||||
from any number of shutdown hooks.
|
||||
"""
|
||||
if self.engine is None:
|
||||
return
|
||||
|
||||
@@ -520,12 +520,14 @@ class Settings:
|
||||
# or supply a private-CA bundle.
|
||||
database_verify_ssl: bool | None = None
|
||||
database_ca_bundle: str | None = None
|
||||
# Postgres connection pool sizing (ADR-026). The asyncpg engine maps
|
||||
# these to its underlying QueuePool. Per-pod max = pool_size +
|
||||
# max_overflow. Defaults are intentionally small (2 + 5 = 7) because
|
||||
# asyncpg connections are single-flight and the typical MCP workload
|
||||
# is light read-mostly point lookups. Validate >= 1 / >= 0 in
|
||||
# __post_init__.
|
||||
# Postgres connection pool sizing — DEPRECATED, retained for
|
||||
# backward compatibility. The asyncpg engine switched to NullPool
|
||||
# in #799 (cross-event-loop crashes under anyio TaskGroups made
|
||||
# the original QueuePool + pool_pre_ping setup unsafe). These
|
||||
# fields no longer affect the Postgres engine; the validators
|
||||
# below still reject invalid values so misconfigured deploys
|
||||
# fail loudly rather than silently. See ADR-026 § Connection
|
||||
# pool and docs/configuration.md.
|
||||
database_pool_size: int = 2
|
||||
database_max_overflow: int = 5
|
||||
|
||||
|
||||
Reference in New Issue
Block a user