ci: harden the qdrant integration job (5s client timeout + cold storage) #60

Merged
jcabillot merged 1 commits from ci/harden-qdrant-integration into main 2026-10-17 17:43:30 -04:00
Owner

Problème

Le job integration-test échoue de façon intermittente (main et PR ouvertes) depuis le 2026-10-17 :

httpcore.ReadTimeout: timed out   # sur PUT /collections/... ou PUT /collections/.../index?wait=true
httpx.ReadTimeout: timed out
qdrant_client.http.exceptions.ResponseHandlingException: timed out

Pendant ce temps GET /collections et /healthz répondent normalement, et tests/test_integration.py n'exerce jamais le transport HTTP/SSE du serveur MCP (il importe get_qdrant_client, search_emails, read_email) — ce n'est donc pas une régression d'une PR, c'est le job qui est fragile.

Preuve : au même moment, la branche main (e883a791, run 12253, job integration-test 62451) échoue à l'identique sur PUT /collections/test_mcp_maildir/index?wait=true, alors que le même job passait à 00:08 (run 12097) et à 21:05 (run 12251). Le probe docker du runner lui-même a même rendu context deadline exceeded dans ce job.

Cause

  1. Le client Qdrant a un timeout par défaut de 5 s (QdrantClient(timeout=None) → 5 s REST/gRPC).
  2. Le job n'attend que /healthz, qui est servi avant que le moteur de stockage accepte les écritures. Sur un runner qui fait plusieurs builds en parallèle, la première création de collection / indexation dépasse 5 s → la fixture module-scope part en ERROR et les 6 tests échouent.

Correctif

  • Pin de l'image qdrant/qdrant:v1.19 (même digest que latest, identique à ce que tourne le cluster) dans docker-build.yaml et main.yaml, + docker-compose.yml ; renovate.json suit maintenant le pin dans les workflows aussi.
  • Attente d'une écriture réelle : la gate crée puis supprime une collection ci_readiness_probe?timeout=60 (60 tentatives), au lieu de se contenter de /healthz ; en cas d'échec les logs du conteneur Qdrant sont dumpés.
  • QDRANT_TIMEOUT (secondes) honoré par get_qdrant_client(), défaut inchangé (5 s) si non défini ; les jobs d'intégration le passent à 60, ils n'ont pas de SLA de latence.
  • Étape de diagnostic (docker ps -a + docker logs --tail 200) ajoutée avant le cleanup, en cas d'échec.

Vérifications

  • ruff check src/ tests/ --select F821 → All checks passed.
  • pytest tests/ -q → 45 passed, 6 deselected.
  • pytest tests/ -q -m integration (contre un vrai Qdrant) → 6 passed in 10.37s.
  • Séquence de readiness testée en réel : PUT /collections/ci_readiness_probe?timeout=60 → 200, DELETE → 200, collections inchangées.
  • Validation YAML de tous les workflows, JSON de renovate.json.
## Problème Le job `integration-test` échoue de façon intermittente (main **et** PR ouvertes) depuis le 2026-10-17 : ``` httpcore.ReadTimeout: timed out # sur PUT /collections/... ou PUT /collections/.../index?wait=true httpx.ReadTimeout: timed out qdrant_client.http.exceptions.ResponseHandlingException: timed out ``` Pendant ce temps `GET /collections` et `/healthz` répondent normalement, et `tests/test_integration.py` n'exerce **jamais** le transport HTTP/SSE du serveur MCP (il importe `get_qdrant_client`, `search_emails`, `read_email`) — ce n'est donc pas une régression d'une PR, c'est le job qui est fragile. Preuve : au même moment, la branche `main` (`e883a791`, run 12253, job integration-test 62451) échoue à l'identique sur `PUT /collections/test_mcp_maildir/index?wait=true`, alors que le même job passait à 00:08 (run 12097) et à 21:05 (run 12251). Le probe docker du runner lui-même a même rendu `context deadline exceeded` dans ce job. ## Cause 1. Le client Qdrant a un **timeout par défaut de 5 s** (`QdrantClient(timeout=None)` → 5 s REST/gRPC). 2. Le job n'attend que `/healthz`, qui est servi **avant** que le moteur de stockage accepte les écritures. Sur un runner qui fait plusieurs builds en parallèle, la première création de collection / indexation dépasse 5 s → la fixture module-scope part en ERROR et les 6 tests échouent. ## Correctif - **Pin de l'image** `qdrant/qdrant:v1.19` (même digest que `latest`, identique à ce que tourne le cluster) dans `docker-build.yaml` et `main.yaml`, + `docker-compose.yml` ; `renovate.json` suit maintenant le pin dans les workflows aussi. - **Attente d'une écriture réelle** : la gate crée puis supprime une collection `ci_readiness_probe?timeout=60` (60 tentatives), au lieu de se contenter de `/healthz` ; en cas d'échec les logs du conteneur Qdrant sont dumpés. - **`QDRANT_TIMEOUT`** (secondes) honoré par `get_qdrant_client()`, défaut inchangé (5 s) si non défini ; les jobs d'intégration le passent à `60`, ils n'ont pas de SLA de latence. - Étape de diagnostic (`docker ps -a` + `docker logs --tail 200`) ajoutée **avant** le cleanup, en cas d'échec. ## Vérifications - `ruff check src/ tests/ --select F821` → All checks passed. - `pytest tests/ -q` → 45 passed, 6 deselected. - `pytest tests/ -q -m integration` (contre un vrai Qdrant) → **6 passed in 10.37s**. - Séquence de readiness testée en réel : `PUT /collections/ci_readiness_probe?timeout=60` → 200, `DELETE` → 200, collections inchangées. - Validation YAML de tous les workflows, JSON de `renovate.json`.
opencodecabilloteu added 1 commit 2026-10-17 17:24:31 -04:00
ci: harden the qdrant integration job
PR Checks / hadolint (pull_request) Successful in 23s
Docker Build and Push / test (pull_request) Successful in 5m19s
PR Checks / build-test (pull_request) Successful in 47s
Docker Build and Push / integration-test (pull_request) Successful in 5m30s
PR Checks / pytest (pull_request) Successful in 3m7s
Docker Build and Push / build (pull_request) Successful in 3m14s
01d15c486e
The integration test job has been failing intermittently on main and on
open pull requests with `httpx.ReadTimeout: timed out` on the first write
to Qdrant, while `GET /collections` and `/healthz` kept answering.

Cause: the qdrant-client default request timeout is 5 seconds, and the job
only waits for `/healthz`, which is served before the storage engine
accepts writes. On a shared runner doing several builds at once, the first
collection creation or indexing exceeds 5s and the whole fixture errors
out.

- pin qdrant/qdrant to v1.19 (the digest `latest` and the cluster already
  run) and let Renovate track the pin in the workflows as well
- wait for a real write round-trip (create + delete a probe collection)
  instead of `/healthz` alone, and dump the container logs if it never
  becomes writable
- make the Qdrant client timeout configurable via QDRANT_TIMEOUT and set
  it to 60s in CI, where there is no latency SLA
opencodecabilloteu requested review from jcabillot 2026-10-17 17:24:39 -04:00
jcabillot merged commit 1115db82ed into main 2026-10-17 17:43:30 -04:00
jcabillot deleted branch ci/harden-qdrant-integration 2026-10-17 17:43:48 -04:00
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: perso/mcp-maildir#60