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:
Collin
2026-06-01 00:57:48 -04:00
committed by GitHub
parent 8218421733
commit 0a7de1fdf4
3 changed files with 94 additions and 24 deletions
+6 -24
View File
@@ -23,7 +23,7 @@ from src.prompt_security import untrusted_context_message
from core.exceptions import SessionNotFoundError
from src.auth_helpers import get_current_user
from routes.session_routes import _verify_session_owner
from core.database import SessionLocal
from core.database import SessionLocal, get_session_mode, set_session_mode
from core.database import Session as DBSession, ChatMessage as DBChatMessage
from core.database import Document as DBDocument, ModelEndpoint
from routes.research_routes import _resolve_research_endpoint
@@ -326,26 +326,14 @@ def setup_chat_routes(
# Check for research_pending BEFORE mode persist overwrites it
do_research = str(use_research).lower() == "true"
if not do_research:
try:
_mode_db = SessionLocal()
_db_mode = _mode_db.query(DBSession.mode).filter(DBSession.id == session).scalar()
_mode_db.close()
if _db_mode == 'research_pending':
do_research = True
logger.info(f"Session {session} in research_pending — auto-triggering research")
except Exception:
pass
if get_session_mode(session) == 'research_pending':
do_research = True
logger.info(f"Session {session} in research_pending — auto-triggering research")
# Persist session mode (research > agent > chat)
_effective_mode = 'research' if do_research else (chat_mode or 'chat')
if _effective_mode in ('agent', 'research', 'chat'):
try:
_mdb = SessionLocal()
_mdb.query(DBSession).filter(DBSession.id == session).update({"mode": _effective_mode})
_mdb.commit()
_mdb.close()
except Exception as _me:
logger.warning("Failed to persist session mode: %s", _me)
set_session_mode(session, _effective_mode)
att_ids = []
if body and isinstance(body.get("attachments"), list):
@@ -547,13 +535,7 @@ def setup_chat_routes(
logger.info(f"First research message — asking clarifying questions for: {message[:60]}")
yield f'data: {json.dumps({"type": "model_info", "model": sess.model, "suffix": "Research"})}\n\n'
# Set DB mode to research_pending so the NEXT message auto-triggers research
try:
_pdb = SessionLocal()
_pdb.query(DBSession).filter(DBSession.id == session).update({"mode": "research_pending"})
_pdb.commit()
_pdb.close()
except Exception as _pe:
logger.warning(f"Failed to set research_pending: {_pe}")
set_session_mode(session, "research_pending")
ctx.messages.insert(0, {"role": "system", "content":
"The user wants to start deep web research. Before searching, ask 2-3 brief "
"clarifying questions to understand exactly what they want to know. For example: "