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
44 lines
1.3 KiB
Python
44 lines
1.3 KiB
Python
"""Runtime timing helpers for EPUB search."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
from time import perf_counter
|
|
from typing import TYPE_CHECKING
|
|
|
|
if TYPE_CHECKING:
|
|
from collections.abc import Awaitable, Callable
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class RuntimeStep:
|
|
"""Elapsed runtime for one named search step."""
|
|
|
|
name: str
|
|
duration_ms: float
|
|
counts_toward_total: bool = True
|
|
|
|
|
|
def runtime_step_from_start(name: str, start_seconds: float) -> RuntimeStep:
|
|
"""Create a runtime step from a prior perf_counter timestamp."""
|
|
return RuntimeStep(name=name, duration_ms=(perf_counter() - start_seconds) * 1000)
|
|
|
|
|
|
def timed_result[T, **P](
|
|
name: str,
|
|
operation: Callable[P, T],
|
|
*args: P.args,
|
|
**kwargs: P.kwargs,
|
|
) -> tuple[T, RuntimeStep]:
|
|
"""Run an operation and return its result plus elapsed runtime."""
|
|
start_seconds = perf_counter()
|
|
result = operation(*args, **kwargs)
|
|
return result, runtime_step_from_start(name, start_seconds)
|
|
|
|
|
|
async def async_timed_result[T](name: str, awaitable: Awaitable[T]) -> tuple[T, RuntimeStep]:
|
|
"""Await an operation and return its result plus elapsed runtime."""
|
|
start_seconds = perf_counter()
|
|
result = await awaitable
|
|
return result, runtime_step_from_start(name, start_seconds)
|