Ingest: unsupported file type is retried 3× and (mis)counted as a 5xx — should be a terminal client-data failure #5
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
When Daedalus sends a file whose type Mnemosyne can't parse (e.g.
.drawio→vnd.jgraph.mxfile, macro-enabled.xlsm→vnd.ms-excel.sheet.macroenabled.12), theingest_from_daedalusCelery task treats the resultingValueErroras a transient error and retries it 3× at 60s intervals before finally marking the jobfailed. An unsupported file type is deterministic — re-parsing identical bytes can never succeed — so all three retries are guaranteed waste.This surfaced via a production alert (
MnemosyneApp5xxErrors, WARNING) that had been firing continuously since 2026-07-13. Investigation showed the app is healthy: no HTTP 5xx are served to any client (alluvicorn.accesslines are 200). The only real errors are these worker-side parse failures. Thedjango_http_responses_total_by_status_total{job="mnemosyne",status="503"}counter increments in lockstep with the retry cycles (flat ~36 retries/6h for the whole week), which is what keeps the alert lit.An unsupported file type is a client/data condition (the input isn't parseable), not a server fault. It should be recorded as a terminal
failedjob with a clear reason and should not be retried, nor classified as a 5xx.Confirmed in code
1. Deterministic failure is on the retry path —
library/tasks.py:468-476:The bare
except Exceptiondoesn't distinguish "transient" (S3 hiccup, Neo4j blip, embedding-server timeout — worth retrying) from "permanent" (unsupported file type, corrupt file — never worth retrying).2. The raise it's catching is deterministic —
library/services/parsers.py:111-114:Proposed fix
Primary — don't retry a permanent parse failure. Introduce a non-retriable exception (e.g.
UnsupportedFileTypeError(ValueError)raised fromparsers.py), and iningest_from_daedalustreat it as terminal:This alone eliminates the retry loop and the steady error signal. (Celery's
Reject/Ignoreorraise self.retrybeing skipped for this class achieves the same; the exact mechanism is the dev team's call.)Secondary — classification. Wherever the failure is reflected as an HTTP status, an unsupported/unparseable input should be 4xx (422 Unprocessable Entity is the precise fit) or simply a normal
failedjob record — not a 5xx. Note: the ingest REST endpoints inlibrary/api/views.py(ingest_create,ingest_job_detail, etc.) do not emit 503 — they use 200/201/202/204/400/404/409/500. I was not able to pin down the exact code path emitting thestatus="503"counter increment (it isn't in the ingest views, and no 503 appears in access logs — so it may be the readiness endpoint, DRF throttling, or an nginx upstream blip coincident with retries). Flagging for triage rather than asserting a location. The metrics correlation with the retry cadence is strong, but the origin is unconfirmed.Optional — normalization gap.
_normalize_file_type(tasks.py:41-53) doesn't mapvnd.ms-excel.sheet.macroenabled.12→xlsx. Since PyMuPDF can open.xlsm, adding this (and.drawiohandling/rejection) to_MIME_TO_EXTwould let some currently-rejected files ingest. Lower priority than stopping the retry loop.Impact
Repro
Ingest any file whose type isn't in
PYMUPDF_EXTENSIONS | PLAINTEXT_EXTENSIONS | IMAGE_EXTENSIONS(e.g. a.drawioor.xlsm) via the Daedalus ingest path. Observe in the worker log:Retry in 60s: ValueError("Unsupported file type ...")repeating 3× before the job settles tofailed.Sample log line (production, altair, 2026-07-20)
Filed from an infra investigation of the
MnemosyneApp5xxErrorsalert. The alert rule itself (Tauruspplg/alert_rules.yml.j2) is also being hardened separately so it keys on genuine500/502/504per-instance rather than any 5xx; that's tracked on the infra side and is independent of this code fix.