phase: 110_fix_sse_db_pool_exhaustion
--- **Phase 110 — Fix SSE DB Connection Pool Exhaustion (SEC-14-04): COMPLETE** **What was implemented/verified:** - All three tasks (pool config, short-lived sessions, concurrency cap) were already implemented in code - Fixed `tests/integration/test_chat_db_sessions.py` — added FakeChatLLM mock, fixed LLM signature (`tools=` not `_tools=`), used `fastapi_app.dependency_overrides` instead of `client.app.dependency_overrides` - Fixed `tests/e2e/test_chat_db_pool.py` — added FakeChatLLM mock, fixed admin password to match `tests/conftest.py`, removed unused imports - Fixed lint errors (unused imports, import order) in both test files **Test / lint / coverage results:** - `uv run pytest` → 2350 passed, 1 warning, 56.4s - `uv run pytest --cov=app --cov-report=term-missing` → 99% coverage (4065 lines, 16 uncovered) - `uv run pytest tests/e2e/test_chat_db_pool.py -v --no-cov` → 3 passed - `uv run pytest tests/integration/test_chat_db_sessions.py -v --no-cov` → 4 passed - `uv run pytest tests/integration/test_chat_concurrency.py -v --no-cov` → 11 passed - `uv run pytest tests/unit/test_db_pool_config.py -v --no-cov` → 14 passed - `uv run pytest tests/unit/test_agent_short_lived_sessions.py -v --no-cov` → 7 passed - `uv run ruff check .` → all checks passed - `uv run pyright` → 0 errors, 0 warnings **Completion criteria:** - [✓] `app/db.py::create_engine` receives explicit `pool_size=5`, `max_overflow=10`, `pool_recycle=3600` from settings - [✓] `run_agent` accepts `db_factory: Callable[[], Session]` and creates short-lived sessions per tool call - [✓] Each tool round uses a separate DB session closed after the tool result - [✓] Concurrency cap (`BOR_CHAT_MAX_CONCURRENT`, default 10) limits concurrent turns; excess get 503 - [✓] All test gates green, coverage 99%, lint/types clean **Notable decisions:** Tests needed LLM mocking (the original test files lacked `FakeChatLLM` mocks, causing hangs on real LLM calls). **Next pending phase:** None — this is the last phase in `todo/`.
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
"""Unit tests for DB pool configuration (SEC-14-04, phase 106, task 01).
|
||||
|
||||
Verifies that:
|
||||
- Settings expose db_pool_size, db_pool_max_overflow, db_pool_recycle
|
||||
with correct defaults and validators.
|
||||
- create_engine() receives the pool kwargs from settings.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
|
||||
import pytest
|
||||
|
||||
from app.config import Settings
|
||||
|
||||
|
||||
class TestSettingsDefaults:
|
||||
"""Pool config defaults match SQLAlchemy implicit defaults."""
|
||||
|
||||
def test_pool_size_default(self):
|
||||
assert Settings().db_pool_size == 5
|
||||
|
||||
def test_pool_max_overflow_default(self):
|
||||
assert Settings().db_pool_max_overflow == 10
|
||||
|
||||
def test_pool_recycle_default(self):
|
||||
assert Settings().db_pool_recycle == 3600
|
||||
|
||||
|
||||
class TestSettingsCustomValues:
|
||||
"""Custom values round-trip correctly."""
|
||||
|
||||
def test_custom_all_three(self):
|
||||
s = Settings(
|
||||
db_pool_size=10,
|
||||
db_pool_max_overflow=20,
|
||||
db_pool_recycle=1800,
|
||||
)
|
||||
assert s.db_pool_size == 10
|
||||
assert s.db_pool_max_overflow == 20
|
||||
assert s.db_pool_recycle == 1800
|
||||
|
||||
def test_custom_pool_size_only(self):
|
||||
s = Settings(db_pool_size=8)
|
||||
assert s.db_pool_size == 8
|
||||
assert s.db_pool_max_overflow == 10
|
||||
assert s.db_pool_recycle == 3600
|
||||
|
||||
|
||||
class TestValidators:
|
||||
"""Pool config validators reject invalid values."""
|
||||
|
||||
def test_pool_size_zero_raises(self):
|
||||
with pytest.raises(ValueError, match="db_pool_size must be >= 1"):
|
||||
Settings(db_pool_size=0)
|
||||
|
||||
def test_pool_size_negative_raises(self):
|
||||
with pytest.raises(ValueError, match="db_pool_size must be >= 1"):
|
||||
Settings(db_pool_size=-5)
|
||||
|
||||
def test_pool_max_overflow_negative_raises(self):
|
||||
with pytest.raises(ValueError, match="db_pool_max_overflow must be >= 0"):
|
||||
Settings(db_pool_max_overflow=-1)
|
||||
|
||||
def test_pool_max_overflow_zero_is_legal(self):
|
||||
s = Settings(db_pool_max_overflow=0)
|
||||
assert s.db_pool_max_overflow == 0
|
||||
|
||||
def test_pool_recycle_zero_is_legal(self):
|
||||
"""pool_recycle=0 means never recycle — legal, just aggressive."""
|
||||
s = Settings(db_pool_recycle=0)
|
||||
assert s.db_pool_recycle == 0
|
||||
|
||||
|
||||
class TestEngineKwargs:
|
||||
"""create_engine() receives the correct pool parameters from settings."""
|
||||
|
||||
def test_create_engine_pool_pre_ping_true(self):
|
||||
"""pool_pre_ping must remain True (connection health check)."""
|
||||
from app import db # noqa: F811
|
||||
|
||||
# The engine's pool options include pool_pre_ping=True.
|
||||
# We verify by checking the pool's _pre_ping attribute.
|
||||
assert db.engine.pool._pre_ping is True
|
||||
|
||||
def test_create_engine_pool_recycle(self):
|
||||
"""pool_recycle defaults to 3600 seconds."""
|
||||
from app import db # noqa: F811
|
||||
|
||||
assert db.engine.pool._recycle == 3600
|
||||
|
||||
def test_sessionlocal_still_callable(self):
|
||||
"""SessionLocal remains a valid session factory."""
|
||||
from app import db # noqa: F811
|
||||
|
||||
assert callable(db.SessionLocal)
|
||||
|
||||
def test_get_db_still_yields_session(self):
|
||||
"""get_db() dependency still yields a Session (contract preserved)."""
|
||||
from app import db # noqa: F811
|
||||
|
||||
gen = db.get_db()
|
||||
session = next(gen)
|
||||
assert isinstance(session, db.Session)
|
||||
session.close()
|
||||
# Generator cleanup
|
||||
with contextlib.suppress(StopIteration):
|
||||
next(gen)
|
||||
Reference in New Issue
Block a user