feat(ebook): migrate to async DB/HTTP and parallelize phrase pipeline
Convert the ebook-search web app to async end to end and add concurrency to the protected-phrase extraction and judging pipeline so large books no longer block the event loop or the UI. ORM / infra: - Add get_async_postgres_engine and factor shared URL/connect_args building into build_postgres_url (reused by the sync and async engine builders) - Add async FastAPI session helpers (get_async_db, AsyncDbSession) with expire_on_commit=False to avoid implicit IO under asyncio App: - Use AsyncEngine/AsyncSession throughout routes, search, ingest, embeddings, answer, rerank and LLM calls; convert handlers to async - Share a single httpx.AsyncClient in app state for LLM requests; size the connection pool for concurrent phrase-judging workers - Add judge_tasks: run per-book judging as tracked background tasks so a book already being judged isn't double-queued Protected phrases: - Add a process pool (pool.py) and worker-count config (extraction/judge book/phrase workers) to parallelize candidate generation and judging - Split admin actions into all/missing variants for generation and judging Config: - Add protected_phrase_extraction_workers, phrase_judge_book_workers, phrase_judge_phrase_workers
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
"""Reusable FastAPI tools."""
|
||||
|
||||
from python.fastapi_tools.db import DbSession, get_db
|
||||
from python.fastapi_tools.db import AsyncDbSession, DbSession, get_async_db, get_db
|
||||
from python.fastapi_tools.zstd_middleware import ZstdMiddleware
|
||||
|
||||
__all__ = ["DbSession", "ZstdMiddleware", "get_db"]
|
||||
__all__ = ["AsyncDbSession", "DbSession", "ZstdMiddleware", "get_async_db", "get_db"]
|
||||
|
||||
@@ -5,10 +5,11 @@ from __future__ import annotations
|
||||
from typing import TYPE_CHECKING, Annotated
|
||||
|
||||
from fastapi import Depends, Request
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Iterator
|
||||
from collections.abc import AsyncIterator, Iterator
|
||||
|
||||
|
||||
def get_db(request: Request) -> Iterator[Session]:
|
||||
@@ -17,4 +18,15 @@ def get_db(request: Request) -> Iterator[Session]:
|
||||
yield session
|
||||
|
||||
|
||||
async def get_async_db(request: Request) -> AsyncIterator[AsyncSession]:
|
||||
"""Get an async database session from app state.
|
||||
|
||||
expire_on_commit=False keeps ORM attributes readable after commit without
|
||||
triggering implicit IO, which would raise under asyncio.
|
||||
"""
|
||||
async with AsyncSession(request.app.state.engine, expire_on_commit=False) as session:
|
||||
yield session
|
||||
|
||||
|
||||
DbSession = Annotated[Session, Depends(get_db)]
|
||||
AsyncDbSession = Annotated[AsyncSession, Depends(get_async_db)]
|
||||
|
||||
Reference in New Issue
Block a user