98 lines
3.2 KiB
Python
98 lines
3.2 KiB
Python
"""Liveness and readiness routes for the EPUB search service."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from http import HTTPStatus
|
|
from typing import TYPE_CHECKING
|
|
|
|
from fastapi import APIRouter
|
|
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.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
|
|
|
|
if TYPE_CHECKING:
|
|
from sqlalchemy.orm import Session
|
|
|
|
from python.ebook_search.config import EbookSearchConfig
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
@router.get("/health")
|
|
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:
|
|
"""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)
|
|
bm25_status = check_bm25_status(config)
|
|
|
|
checks = {
|
|
"database": "ok" if database_ok else "fail",
|
|
"embedding": "ok" if embedding_ok else "fail",
|
|
"chat": chat_status,
|
|
"bm25": bm25_status,
|
|
}
|
|
if not database_ok:
|
|
status = "unavailable"
|
|
status_code = HTTPStatus.SERVICE_UNAVAILABLE
|
|
elif not embedding_ok or chat_status == "fail" or bm25_status == "missing":
|
|
status = "degraded"
|
|
status_code = HTTPStatus.OK
|
|
else:
|
|
status = "ready"
|
|
status_code = HTTPStatus.OK
|
|
|
|
logger.info(
|
|
"ebook_ready_check status=%s database=%s embedding=%s chat=%s bm25=%s",
|
|
status,
|
|
database_ok,
|
|
embedding_ok,
|
|
chat_status,
|
|
bm25_status,
|
|
)
|
|
return JSONResponse(content={"status": status, "checks": checks}, status_code=status_code)
|
|
|
|
|
|
def chat_endpoint_status(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"
|
|
|
|
|
|
def check_database(session: Session) -> bool:
|
|
"""Return whether the database answers a trivial query."""
|
|
try:
|
|
session.execute(select(literal(1)))
|
|
except SQLAlchemyError as error:
|
|
logger.warning("ebook_ready_database_unavailable error=%s", error)
|
|
return False
|
|
return True
|
|
|
|
|
|
def check_bm25_status(config: EbookSearchConfig) -> str:
|
|
"""Return the persisted BM25 index status without loading it into memory."""
|
|
index_path = bm25_index_path(config)
|
|
manifest = read_bm25_manifest(index_path)
|
|
if manifest is None or not bm25_index_exists(index_path, manifest):
|
|
return "missing"
|
|
if manifest.chunk_count == 0:
|
|
return "empty"
|
|
return "ok"
|