Fix flaky extract-pipeline E2E: warm cold-start ingest at the root#1857
Fix flaky extract-pipeline E2E: warm cold-start ingest at the root#1857JSv4 wants to merge 8 commits into
Conversation
The `Extract pipeline (PDF upload → run → CSV)` check red-X'd intermittently across PRs (#1825/#1835/#1836). It failed not on an assertion but on a JOB CANCELLATION: the first document ingest stalled past the 14-minute per-document wait, and that attempt (~18 min) plus the Playwright retry (~3.6 min) blew the job's 30-minute `timeout-minutes`. Playwright itself reported the run as flaky-passed. Root cause is a cold start, not gradual slowness: Docling loads its layout/table/OCR models lazily on the first `/parse/` call, and on a cold 2-core runner already hosting the docling + multimodal + privacy-filter images that first parse balloons into a multi-minute, highly variable hang (the warm retry does the entire flow in 3.6 min). The vector embedder, by contrast, is ready in seconds. The prior fix (#1846) only raised the wait 8->14 min, deferring the symptom into the job timeout rather than removing it. Three changes attack the cause: - Warm-up step (frontend-e2e-extract.yml): run one real Docling parse of the small committed usc-title-1.pdf fixture plus an embedder call before the timed spec, so both ingests run warm. Best-effort — warm-up failures warn but do not fail the job. - Readiness gating (local.yml): add /health healthchecks to vector-embedder, multimodal-embedder, and docling-parser, and gate django + celeryworker on vector-embedder / docling-parser being service_healthy (was service_started, so the worker could start parsing before the services were listening). - CELERY_WORKER_CONCURRENCY (worker start script): honor this env var (default 1, unchanged for local dev); the extract workflow sets it to 2 so the two uploaded PDFs parse + embed concurrently instead of serialising behind a single child. Healthcheck commands verified against the real images (python urllib /health -> 200; both flip to healthy). compose config validates; the warm-up heredoc passes bash -n and py_compile.
Code ReviewOverviewThis PR addresses a well-diagnosed root cause: Docling's lazy model loading on first IssuesBug: Warm-up step missing
|
First fix attempt (warm-up of docling/embedder microservices) did NOT resolve the stall: CI showed `docling warmup parse: 200 in 31.3s` (models warm) yet attempt 1 still took 15 min and the job was cancelled at the 30-min cap. So the docling microservice cold-load is NOT the bottleneck. The thing the warm-up did not touch is the celeryworker's own first-task processing (the warm-up hit docling/embedder from the django container, not through the worker), and the prime remaining suspect is host memory thrash (concurrency=2 worker children + the 7 GB multimodal-embedder the extract test never uses, on a 2-core/7 GB runner). We were debugging blind: on cancellation the `failure()` log-capture steps were skipped, so no worker logs were ever uploaded. This commit is observability only (no fix): - Capture step -> `if: always()` (runs on cancellation) and now also dumps celeryworker.log + django.log (timestamped), `docker stats`, `free -h`/meminfo, and dmesg OOM hits. - Disable Playwright retry (`--retries=0`) for this diagnostic pass so a cold stall fails in-budget, Playwright prints which step/doc failed, and the capture uploads cleanly instead of racing the job cancel. Revert the diagnostic retry change once the root cause is confirmed.
|
test comment |
Code ReviewSummary: Well-diagnosed fix for a subtle cold-start flakiness issue. The three-pronged approach (warm-up, healthcheck gating, concurrency) addresses the root cause correctly. A few things worth addressing before merge. Issues1. Warm-up step is not actually best-effort at the shell levelThe PR description says "warm-up failures warn but don't fail the job", and the Python script handles failures gracefully via Add - name: Warm up Docling parser + embedder (load models off the test clock)
continue-on-error: true # add this
run: |
docker compose -f local.yml exec -T django python - <<'PY'
...Without it, a docker-level failure breaks the job before the spec runs — the opposite of the stated intent. 2.
|
…start-ingest # Conflicts: # CHANGELOG.md
…e, shorten health budget, track retries - Add continue-on-error: true to the warm-up step so a docker-level failure (container crashed before the step) can't block the job — the warm-up is an optimisation, never a gate (review #1, the main blocker). - Document why multimodal-embedder is gated service_started (not service_healthy) and is not a direct celeryworker dependency: it serves image embeddings, off the text/PDF ingest path this pipeline exercises, and gating on its health would add ~7GB-image boot latency to every local up (review #2). - Shorten the warm-up wait_health budget 180s -> 30s now that depends_on gates the services healthy before the step runs (review #4). - Turn the --retries=0 note into a tracked TODO(#1844 follow-up) so the diagnostic setting doesn't silently become permanent (review #3).
Code ReviewOverviewThis PR addresses the root cause of the flaky Positives
Issues / Concerns1. The TODO comment acknowledges this needs to be reverted once the fix proves stable, but there is nothing to ensure that happens. A transient network blip will now unconditionally red-X the job instead of being absorbed by a retry. Recommend either opening a follow-up issue (the TODO references 2. Warm-up timeout of 600s is long relative to the 30-minute job budget r = urllib.request.urlopen(req, timeout=600)If Docling hangs during the warm-up (e.g. OOM on a cold runner), this step blocks for up to 10 minutes before 3. Unrelated binary file in the diff This screenshot appears unrelated to the CI fix. Project conventions commit auto-screenshots via the 4. - name: Upload backend logs + stats
if: always()
uses: actions/upload-artifact@v7Consider 5. File handle not closed in warm-up script "pdf_base64": base64.b64encode(open(pdf, "rb").read()).decode(),Minor: 6. Hardcoded concurrency value tied to current test structure echo "CELERY_WORKER_CONCURRENCY=2" >> .envs/.local/.djangoThe value 2 is chosen because the test uploads exactly 2 PDFs. If the test grows, this needs updating. A brief comment linking the value to the document count would make the relationship explicit and prevent silent staleness. Minor Nits
SummaryThe root-cause analysis is solid and the three-part fix is the right approach. Main actionable items:
|
…start-ingest # Conflicts: # CHANGELOG.md
The maps--annotation-map--pin-panel.png auto-screenshot belongs to the AnnotationMap feature (#1820), not this CI fix. Auto-screenshots are committed via the screenshots.yml workflow, not in feature PRs.
…start-ingest # Conflicts: # CHANGELOG.md
…re/cancel, tidy file handle + concurrency note - Cap the Docling warm-up parse at 300s (was 600s): a warm-up hang past ~5 min is almost certainly an OOM on the cold runner, so fail fast instead of burning a third of the 30-min job budget before the spec. - Capture + upload backend logs on 'failure() || cancelled()' instead of 'always()': still covers the timeout-cancellation gap this PR targets while avoiding artifact churn on green runs. - Read the warm-up PDF via 'with open(...)' so the handle is closed. - Note that CELERY_WORKER_CONCURRENCY=2 is tied to the spec's two PDF fixtures so the relationship doesn't go stale.
Code ReviewOverviewThis PR fixes a genuinely tricky intermittent CI failure by addressing the root cause (lazy Docling model loading on first parse call) rather than just bumping a timeout again. The root-cause analysis in the PR description is thorough and persuasive — the "warm retry did the whole 2-upload flow in 3.6 min" observation is compelling evidence that cold-start is the real culprit. The three-part fix (warm-up step + readiness gating + concurrency unlock) is well-layered. Changes are non-breaking for local dev. StrengthsRoot-cause fix, not symptom deferral.
Best-effort warm-up ( Healthchecks benefit local dev too, not just CI. Cleaner startup ordering is a general improvement. Concerns1. The TODO comment ( 2. Warm-up parse timeout (300 s) eats job budget on OOM. The comment correctly notes that a 5-min hanging warm-up is "almost certainly an OOM." But with 3. Adding the healthcheck is fine for observability and future use, and the comment explains why it is deliberately 4. Healthcheck retries are generous — may slow down failure detection in local dev.
5. Hardcoded This is already in Minor / Nits
SummarySolid, well-analysed fix. The mechanics are correct, the documentation is unusually thorough, and the changes are scoped appropriately. The main ask is making sure |
Summary
The
Extract pipeline (PDF upload → run → CSV)check (.github/workflows/frontend-e2e-extract.yml) red-X'd intermittently across multiple branches (#1825 / #1835 / #1836). This fixes the root cause inmainso every PR benefits. Replaces the symptom-deferral in #1846 (issue #1844).Root cause
The failing run did not fail an assertion — Playwright reported the test as flaky-passed ("1 flaky", exit 0). The red ✗ is a job cancellation: the first document ingest stalled past the 14-minute per-document wait, and that attempt (~18 min) plus the Playwright retry (~3.6 min) exceeded the job's
timeout-minutes: 30, so GitHub cancelled the job mid-cleanup.The stall is a cold start, not gradual slowness:
/parse/call. On a cold 2-core runner already hosting the docling (14 GB) + multimodal-embedder (7 GB) + privacy-filter (6 GB) images, that first parse balloons into a multi-minute, highly variable hang. The warm retry does the entire flow — 2 uploads, both ingests, extract, CSV — in 3.6 min.--concurrency=1, so the two PDFs (incl. the large US Code fixture) parse + embed serially; the second doc (Eton) is what times out, queued behind the first.Changes
frontend-e2e-extract.yml) — runs one real Docling parse of the small committedusc-title-1.pdffixture + an embedder call before the timed spec, so both ingests run warm. Best-effort: warm-up failures warn but don't fail the job.local.yml) —/healthhealthchecks onvector-embedder,multimodal-embedder,docling-parser;django+celeryworkernow gate onvector-embedder/docling-parserbeingservice_healthy(wasservice_started). Benefits every environment, not just CI.CELERY_WORKER_CONCURRENCY(compose/local/django/celery/worker/start) — worker honors this env var (default1, unchanged for local dev); the extract workflow sets it to2so the two PDFs parse + embed concurrently.Verification
docker compose -f local.yml configvalidates.python urllib /health → 200for all three;vector-embedder+docling-parserboth flip tohealthy./embeddings/batch → 200).bash -nandpy_compile; pre-commit clean.Extract pipelinerun — it touches the workflow + helpers paths, so the check runs here.Closes #1844