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:
Justin Visser 2026-03-02 19:09:06 +01:00
parent a490e14126
commit f50b609a36
11 changed files with 97 additions and 69 deletions

View file

@ -8,13 +8,13 @@ from typing import Any
from sqlalchemy import select
from app.db.background_session import background_session
from app.db.models import (
CrawlRun,
RunTriggerType,
User,
UserSetting,
)
from app.db.session import get_session_factory
from app.logging_utils import structured_log
from app.services.ingestion.application import (
RunAlreadyInProgressError,
@ -148,8 +148,7 @@ class SchedulerService:
await self._run_candidate(candidate)
async def _load_candidate_rows(self) -> list[Any]:
session_factory = get_session_factory()
async with session_factory() as session:
async with background_session() as session:
result = await session.execute(
select(
UserSetting.user_id,
@ -204,8 +203,7 @@ class SchedulerService:
return candidates
async def _is_due(self, candidate: _AutoRunCandidate, *, now: datetime) -> bool:
session_factory = get_session_factory()
async with session_factory() as session:
async with background_session() as session:
result = await session.execute(
select(CrawlRun.start_dt)
.where(
@ -227,8 +225,7 @@ class SchedulerService:
*,
candidate: _AutoRunCandidate,
):
session_factory = get_session_factory()
async with session_factory() as session:
async with background_session() as session:
ingestion = ScholarIngestionService(source=self._source)
try:
return await ingestion.run_for_user(
@ -294,8 +291,7 @@ class SchedulerService:
if is_budget_cooldown_active():
return
session_factory = get_session_factory()
async with session_factory() as session:
async with background_session() as session:
try:
processed = await drain_ready_jobs(
session,