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:
@@ -11,15 +11,17 @@ from fastapi.responses import JSONResponse
|
||||
from sqlalchemy import literal, select
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
|
||||
from python.ebook_search.api.dependencies import (
|
||||
AppConfig, # noqa: TC001 FastAPI resolves this annotated dependency at runtime
|
||||
from python.ebook_search.api.dependencies import ( # noqa: TC001 FastAPI resolves these annotated dependencies at runtime
|
||||
AppConfig,
|
||||
AppHttpClient,
|
||||
)
|
||||
from python.ebook_search.bm25_corpus import bm25_index_exists, bm25_index_path, read_bm25_manifest
|
||||
from python.ebook_search.llm_interface import check_chat_endpoint, check_embedding_endpoint
|
||||
from python.fastapi_tools import DbSession # noqa: TC001 FastAPI resolves this annotated dependency at runtime
|
||||
from python.fastapi_tools import AsyncDbSession # noqa: TC001 FastAPI resolves this annotated dependency at runtime
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from sqlalchemy.orm import Session
|
||||
import httpx
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from python.ebook_search.config import EbookSearchConfig
|
||||
|
||||
@@ -29,17 +31,17 @@ router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/health")
|
||||
def health() -> dict[str, str]:
|
||||
async def health() -> dict[str, str]:
|
||||
"""Liveness probe that returns ok without touching dependencies."""
|
||||
return {"status": "ok"}
|
||||
|
||||
|
||||
@router.get("/ready")
|
||||
def ready(config: AppConfig, session: DbSession) -> JSONResponse:
|
||||
async def ready(config: AppConfig, session: AsyncDbSession, client: AppHttpClient) -> JSONResponse:
|
||||
"""Readiness probe reporting database, embedding endpoint, and BM25 index status."""
|
||||
database_ok = check_database(session)
|
||||
embedding_ok = check_embedding_endpoint(config)
|
||||
chat_status = chat_endpoint_status(config)
|
||||
database_ok = await check_database(session)
|
||||
embedding_ok = await check_embedding_endpoint(client, config)
|
||||
chat_status = await chat_endpoint_status(client, config)
|
||||
bm25_status = check_bm25_status(config)
|
||||
|
||||
checks = {
|
||||
@@ -69,17 +71,17 @@ def ready(config: AppConfig, session: DbSession) -> JSONResponse:
|
||||
return JSONResponse(content={"status": status, "checks": checks}, status_code=status_code)
|
||||
|
||||
|
||||
def chat_endpoint_status(config: EbookSearchConfig) -> str:
|
||||
async def chat_endpoint_status(client: httpx.AsyncClient, config: EbookSearchConfig) -> str:
|
||||
"""Return the answering chat endpoint status, or disabled when answers are off."""
|
||||
if not config.answer_enabled:
|
||||
return "disabled"
|
||||
return "ok" if check_chat_endpoint(config) else "fail"
|
||||
return "ok" if await check_chat_endpoint(client, config) else "fail"
|
||||
|
||||
|
||||
def check_database(session: Session) -> bool:
|
||||
async def check_database(session: AsyncSession) -> bool:
|
||||
"""Return whether the database answers a trivial query."""
|
||||
try:
|
||||
session.execute(select(literal(1)))
|
||||
await session.execute(select(literal(1)))
|
||||
except SQLAlchemyError as error:
|
||||
logger.warning("ebook_ready_database_unavailable error=%s", error)
|
||||
return False
|
||||
|
||||
Reference in New Issue
Block a user