fix: stop leaking DB connections when persisting session mode (#64)
chat_routes.py persisted a session's "mode" in three best-effort spots — reading the current mode, writing the effective mode, and setting research_pending on the stream path. Each opened a session with SessionLocal() and called .close() as the LAST statement inside a try/except, so if anything before close() raised (e.g. a SQLite "database is locked" under concurrent chat streams) the except only logged and the connection was never returned to the pool. DATABASE_URL defaults to file-backed SQLite, whose engine uses SQLAlchemy's default QueuePool (5 connections + 10 overflow). Repeated leaks on these hot paths exhaust the pool; later requests then block for pool_timeout and fail with "QueuePool limit ... reached", taking the app down until restart. Move the logic into two best-effort helpers in core.database, next to the existing session helpers (update_session_last_accessed, get_session_by_id): - get_session_mode(session_id) -> Optional[str] - set_session_mode(session_id, mode) -> bool Both route through the existing get_db_session() context manager, which commits on success, rolls back on error, and always closes in a finally, so the connection is returned to the pool on every path. chat_routes.py now calls these instead of hand-rolling sessions, also removing three copies of the same try/except. Add tests/test_session_mode_helpers.py: the helpers commit+close on success and, on a mid-operation DB error, swallow + roll back + close (no leak). The error-path tests fail against the old close()-inside-try pattern.
This commit is contained in:
@@ -1755,6 +1755,33 @@ def update_session_last_accessed(session_id: str):
|
||||
return True
|
||||
return False
|
||||
|
||||
def get_session_mode(session_id: str):
|
||||
"""Return a session's persisted `mode`, or None if unset/unknown.
|
||||
|
||||
Best-effort: never raises (returns None on any DB error) so callers on hot
|
||||
request paths needn't guard it. Routed through get_db_session() so the
|
||||
connection is always returned to the pool."""
|
||||
try:
|
||||
with get_db_session() as db:
|
||||
return db.query(Session.mode).filter(Session.id == session_id).scalar()
|
||||
except Exception:
|
||||
logger.warning("Failed to read mode for session %s", session_id)
|
||||
return None
|
||||
|
||||
def set_session_mode(session_id: str, mode: str) -> bool:
|
||||
"""Persist a session's `mode`. Best-effort: never raises, returns success.
|
||||
|
||||
Routed through get_db_session() so a failure mid-write (e.g. a SQLite
|
||||
'database is locked' under concurrent streams) still returns the connection
|
||||
to the pool instead of leaking it — repeated leaks would exhaust it."""
|
||||
try:
|
||||
with get_db_session() as db:
|
||||
db.query(Session).filter(Session.id == session_id).update({"mode": mode})
|
||||
return True
|
||||
except Exception:
|
||||
logger.warning("Failed to persist mode %r for session %s", mode, session_id)
|
||||
return False
|
||||
|
||||
def get_session_by_id(session_id: str):
|
||||
"""Get a session by ID"""
|
||||
with get_db_session() as db:
|
||||
|
||||
Reference in New Issue
Block a user