fix(db,frontend): reserve DB connections for API and reduce frontend polling
Background tasks (ingestion, PDF resolution, scheduler) now acquire an asyncio.Semaphore before checking out a DB connection, guaranteeing at least N connections remain available for API request handlers. Removes duplicate polling loop from PublicationsPage, debounces publication reload on run-status changes, and increases poll interval from 5s to 15s since SSE handles live discovery. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
a490e14126
commit
f50b609a36
11 changed files with 97 additions and 69 deletions
57
app/db/background_session.py
Normal file
57
app/db/background_session.py
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
"""Semaphore-gated DB sessions for background tasks.
|
||||
|
||||
Background tasks (ingestion, PDF resolution, scheduler) acquire a semaphore
|
||||
permit before checking out a database connection. The semaphore cap is
|
||||
``pool_size + max_overflow - reserved_for_api``, which guarantees that at
|
||||
least *reserved_for_api* connections remain available for API request
|
||||
handlers at all times.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from collections.abc import AsyncIterator
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.db.session import get_session_factory
|
||||
from app.logging_utils import structured_log
|
||||
from app.settings import settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_semaphore: asyncio.Semaphore | None = None
|
||||
|
||||
|
||||
def _build_semaphore() -> asyncio.Semaphore:
|
||||
pool_capacity = max(1, settings.database_pool_size) + max(0, settings.database_pool_max_overflow)
|
||||
reserved = max(0, settings.database_reserved_api_connections)
|
||||
limit = max(1, pool_capacity - reserved)
|
||||
structured_log(
|
||||
logger,
|
||||
"info",
|
||||
"db.background_semaphore_initialized",
|
||||
pool_capacity=pool_capacity,
|
||||
reserved_for_api=reserved,
|
||||
background_limit=limit,
|
||||
)
|
||||
return asyncio.Semaphore(limit)
|
||||
|
||||
|
||||
def get_background_semaphore() -> asyncio.Semaphore:
|
||||
global _semaphore
|
||||
if _semaphore is None:
|
||||
_semaphore = _build_semaphore()
|
||||
return _semaphore
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def background_session() -> AsyncIterator[AsyncSession]:
|
||||
"""Yield a DB session after acquiring the background semaphore."""
|
||||
semaphore = get_background_semaphore()
|
||||
async with semaphore:
|
||||
session_factory = get_session_factory()
|
||||
async with session_factory() as session:
|
||||
yield session
|
||||
Loading…
Add table
Add a link
Reference in a new issue