fix(qdrant): verify collection existence in the readiness check
Why: - The chunks collection is now created by an explicit deployment step (qdrant_bootstrap), not at startup, which means a process can boot against a healthy Qdrant that has no collection at all. /readyz's previous check only called get_collections(), so it reported ready in that state — the misconfiguration stayed invisible until the first upload failed with a 502 after already paying for the MinIO write and embedding round trips. Changes: - ping_qdrant() now checks collection_exists(collection) instead of just reachability.
This commit is contained in:
@@ -23,7 +23,9 @@ async def readyz(request: Request, response: Response) -> dict[str, bool]:
|
|||||||
postgres_ready, minio_ready, qdrant_ready = await asyncio.gather(
|
postgres_ready, minio_ready, qdrant_ready = await asyncio.gather(
|
||||||
ping_postgres(resources.db_engine, timeout),
|
ping_postgres(resources.db_engine, timeout),
|
||||||
ping_minio(resources.minio_client, timeout),
|
ping_minio(resources.minio_client, timeout),
|
||||||
ping_qdrant(resources.qdrant_client, timeout),
|
ping_qdrant(
|
||||||
|
resources.qdrant_client, timeout, collection=resources.settings.qdrant.collection
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
result = {
|
result = {
|
||||||
|
|||||||
@@ -9,10 +9,22 @@ def create_client(settings: QdrantSettings) -> AsyncQdrantClient:
|
|||||||
return AsyncQdrantClient(url=settings.url, api_key=settings.api_key)
|
return AsyncQdrantClient(url=settings.url, api_key=settings.api_key)
|
||||||
|
|
||||||
|
|
||||||
async def ping(client: AsyncQdrantClient, timeout: float) -> bool:
|
async def ping(client: AsyncQdrantClient, timeout: float, *, collection: str) -> bool:
|
||||||
|
"""Whether Qdrant is reachable **and** the `chunks` collection exists.
|
||||||
|
|
||||||
|
Reachability alone is not readiness here. The collection is created by a
|
||||||
|
deployment step (`python -m src.cli.qdrant_bootstrap`, see ADR-0001
|
||||||
|
"Collection provisioning"), so a process can boot against a healthy Qdrant
|
||||||
|
that has no collection at all. Without this check that misconfiguration
|
||||||
|
stays invisible until the first upload fails with a `502` — after the
|
||||||
|
request has already paid for the MinIO write and the embedding round trips.
|
||||||
|
|
||||||
|
This is the Qdrant analogue of an unapplied Alembic migration, and it
|
||||||
|
belongs in `/readyz` for the same reason: it is a dependency-readiness
|
||||||
|
condition, not a process-health one.
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
async with asyncio.timeout(timeout):
|
async with asyncio.timeout(timeout):
|
||||||
await client.get_collections()
|
return await client.collection_exists(collection)
|
||||||
except Exception:
|
except Exception:
|
||||||
return False
|
return False
|
||||||
return True
|
|
||||||
|
|||||||
45
tests/integration/qdrant/test_readiness.py
Normal file
45
tests/integration/qdrant/test_readiness.py
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
"""`/readyz`'s Qdrant probe against a real Qdrant.
|
||||||
|
|
||||||
|
The case worth an integration test is the one a fake cannot produce
|
||||||
|
convincingly: Qdrant is up and answering, but the collection the deployment
|
||||||
|
step was supposed to create is not there.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from qdrant_client import AsyncQdrantClient
|
||||||
|
|
||||||
|
from src.config import QdrantSettings
|
||||||
|
from src.infrastructure.qdrant.client import ping
|
||||||
|
from src.infrastructure.qdrant.collection import ensure_chunks_collection
|
||||||
|
|
||||||
|
pytestmark = [
|
||||||
|
pytest.mark.integration,
|
||||||
|
pytest.mark.qdrant,
|
||||||
|
pytest.mark.asyncio(loop_scope="session"),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
async def test_ping_is_not_ready_when_the_collection_was_never_bootstrapped(
|
||||||
|
qdrant_client: AsyncQdrantClient, qdrant_settings: QdrantSettings
|
||||||
|
) -> None:
|
||||||
|
"""A healthy Qdrant with no collection is *not* ready: uploads would 502."""
|
||||||
|
assert await ping(qdrant_client, 5.0, collection=qdrant_settings.collection) is False
|
||||||
|
|
||||||
|
|
||||||
|
async def test_ping_is_ready_after_bootstrap(
|
||||||
|
qdrant_client: AsyncQdrantClient, qdrant_settings: QdrantSettings
|
||||||
|
) -> None:
|
||||||
|
await ensure_chunks_collection(qdrant_client, collection=qdrant_settings.collection)
|
||||||
|
|
||||||
|
assert await ping(qdrant_client, 5.0, collection=qdrant_settings.collection) is True
|
||||||
|
|
||||||
|
|
||||||
|
async def test_ping_is_not_ready_for_a_different_collection_name(
|
||||||
|
qdrant_client: AsyncQdrantClient, qdrant_settings: QdrantSettings
|
||||||
|
) -> None:
|
||||||
|
"""A misconfigured QDRANT_COLLECTION is as unready as a missing one."""
|
||||||
|
await ensure_chunks_collection(qdrant_client, collection=qdrant_settings.collection)
|
||||||
|
|
||||||
|
assert await ping(qdrant_client, 5.0, collection=f"absent_{uuid.uuid4().hex}") is False
|
||||||
Reference in New Issue
Block a user