mirror of
https://github.com/yogeshojha/rengine.git
synced 2026-10-20 07:11:28 +02:00
The SSE route authenticated through CurrentUser, whose session dependency is only released when the response ends — so every open stream left one connection idle in transaction for as long as the tab stayed open. The engine was created with no pool arguments at all, so the ceiling was SQLAlchemy's default 5+10: fifteen tabs and every DB endpoint blocked for pool_timeout and then 500'd. Reproduced exactly, and both pinned backends' last query was get_current_user's SELECT users. StreamUser opens and closes its own short session, so a stream holds nothing pooled — 200 concurrent streams now report checked_out 1. The pool is configured rather than defaulted (10+20, pre-ping, 30m recycle, fail-fast timeout) and carries an application_name, because a blank one is why this needed a live reproduction to diagnose. idle_in_transaction_session_timeout is the backstop: any future leak from any path is reclaimed by Postgres instead of accumulating, and pre-ping makes the next checkout succeed on a fresh socket. A stream also no longer outlives its token. It re-checks expiry and revocation on each heartbeat — JWT plus Redis, no DB — and emits `event: unauthorized`, so logout actually ends the feed and the client refreshes at once instead of burning three failed reconnects. Worker pools are sized per prefork child (8 x 15 was over max_connections) and the inherited engine is disposed on fork so a child never shares a parent socket. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015biqVXTng75nnfKXMqm61H
30 lines
860 B
Python
30 lines
860 B
Python
"""Sync database session for Celery workers."""
|
|
|
|
from sqlalchemy import create_engine
|
|
from sqlalchemy.orm import Session, sessionmaker
|
|
|
|
from app.config import settings
|
|
|
|
# each prefork child inherits this engine and is single-threaded, so the pool is
|
|
# sized per child: concurrency x (pool_size + max_overflow) shares max_connections
|
|
engine = create_engine(
|
|
settings.database_url,
|
|
echo=settings.DEBUG,
|
|
pool_pre_ping=True,
|
|
pool_size=settings.WORKER_DB_POOL_SIZE,
|
|
max_overflow=settings.WORKER_DB_MAX_OVERFLOW,
|
|
pool_timeout=settings.WORKER_DB_POOL_TIMEOUT,
|
|
pool_recycle=settings.DB_POOL_RECYCLE,
|
|
connect_args={"application_name": f"{settings.APP_NAME}-worker"},
|
|
)
|
|
|
|
SyncSessionLocal = sessionmaker(
|
|
bind=engine,
|
|
class_=Session,
|
|
expire_on_commit=False,
|
|
)
|
|
|
|
|
|
def get_sync_session() -> Session:
|
|
return SyncSessionLocal()
|