Skip to content

A transform refusal about the recording is filed as an infrastructure failure #203

Closed

Description

@kstonekuan

What happens

classify_ingest_failure (src/hflow/ingest_ledger.py:80) sorts an ingest failure into one of
three kinds. It returns source-missing for SourceNotFound, source-unreadable for any
McapError, and infrastructure for everything else.

Everything else includes every refusal the transform makes about the file's contents:

  • non-conforming pass-through video (format is not h264, more than one picture per message, a
    keyframe without SPS and PPS, a channel starting mid-GOP)
  • a raw-image schema (sensor_msgs/msg/Image, foxglove.RawImage)
  • an unsupported compressed-image format
  • a derived topic colliding with a source topic

Each of those is a ValueError naming exactly what is wrong with the recording, and each is
filed under a kind that says the problem is with the machine.

Why it matters

The infrastructure kind is what a retry is for. A corrupt or unsupported recording will fail
identically on every retry, so an operator draining the ledger by kind retries a batch that
cannot succeed, and a converter author debugging their output is told the fault is on our side.

docs/how-to/write-a-converter.md (added in #202) currently routes around this by telling
converter authors to read error_type and message and ignore failure_kind. That is good
advice for today and a poor place to leave it: the column exists so nobody has to parse
messages.

Why it is not just a missing branch

classify_ingest_failure's own docstring says the classification is a heuristic, stored beside
the verbatim error_type and message so a wrong guess stays visible. That design is
deliberate and I think worth keeping. The question is whether the heuristic can be improved without
becoming a list of exception messages, which is the thing it was written to avoid.

Three shapes, and the choice is not obvious:

  1. A dedicated exception type for source-content refusals. The transform raises something
    like SourceNotConforming(ValueError) for the refusals above, and the classifier tests for
    it the way it already tests for SourceNotFound. Most precise, and it means the transform
    states the classification at the point that knows it. Costs a new public exception name and
    touching every raise site.
  2. A fourth kind, source-unsupported, distinguishing "this recording is not something v1
    can ingest" from "this recording is not readable at all". Useful to an operator, since the
    first is a converter bug and the second is a corrupt file. Needs the same reclassification
    work as (1) to route anything into it.
  3. Leave the kinds and document them. Defensible, and it is where docs: document input MCAP converter contract #202 left things, but it
    makes failure_kind a column nobody can act on.

(1) and (2) compose: a new exception type could route to a new kind.

Definition of done

Whichever shape is chosen: a transform refusal about the source file's contents is not
classified infrastructure, an McapError still classifies as source-unreadable, and
SourceNotFound still classifies as source-missing. A test per refusal category above pins
the kind it lands in. docs/how-to/write-a-converter.md's paragraph about reading error_type
instead of failure_kind is rewritten or removed to match. If a new kind is added,
docs/CATALOG.md's ingest-failure section documents it.

Validation

uv sync --locked --all-extras
uv run ruff check --fix
uv run ruff format
uv run ty check
uv run pytest -q

Run plain uv run pytest -q from the repo root: pytest tests alone misses
packages/hflow-server/tests. Markdown changes also want the lychee command from
CONTRIBUTING.md.

Notes

Activity

  1. added
    bugSomething isn't working
    help wantedExtra attention is needed
    advancedNeeds codebase familiarity; not a starter issue
    on Aug 27, 2026
  2. aleks-drozy commented on Aug 30, 2026

    @aleks-drozy

    Hi, I'd like to work on this — planning to add a SourceNotConforming exception and a new source-unsupported classification kind, mapped in classify_ingest_failure() and excluded from retry logic, per the direction in the issue. Will follow up with a PR.

  3. kstonekuan commented on Aug 30, 2026

    @kstonekuan
    ContributorAuthor

    Assigned, go ahead.

    Two things to settle before the code, since both are easier now than after.

    failure_kind is a stored column, so a fourth value splits the corpus: rows written before your change keep infrastructure for refusals that'd now be source-unsupported, and nothing rewrites them. Say in the PR what an operator draining an old ledger should expect, and whether anything needs to say so out loud.

    The other is the boundary. The issue lists four refusals that belong on the recording, not the machine, and the classifier has to tell them apart from a genuine ValueError raised by a bug in our own code. A new exception type is the right instrument. What matters is that raising it is the only way into the new kind, so a stray ValueError can't drift in later. Say how you got that.

    Prove the classification with a test that fails if a refusal goes back to infrastructure, and keep the existing three kinds behaving exactly as they do.

  4. aleks-drozy commented on Aug 30, 2026

    @aleks-drozy

    Opened as a draft: #288

  5. aleks-drozy commented on Aug 31, 2026

    @aleks-drozy

    Thanks — answering both before the review, and pushed a follow-up commit (78b59c1) covering them in the PR:

    Old ledger rows: they don't get reclassified. ingest_failures is append-only, so a refusal recorded as infrastructure by an ingest binary from before this change stays infrastructure forever — nothing rewrites past rows. An operator draining an old ledger by kind should expect pre-cutover infrastructure rows to be a mix of genuine infra trouble and content refusals; error_type/message are the ground truth for any individual row regardless of failure_kind, which is exactly why they're kept verbatim beside it. Only rows written by a binary that already raises SourceNotConforming can carry source-unsupported. Spelled this out in docs/CATALOG.md.

    Boundary / the only way in: classify_ingest_failure routes to SOURCE_UNSUPPORTED via a single isinstance(error, SourceNotConforming) check — no string-matching, no other predicate. So a future content refusal that's raised as a bare ValueError (a bug, or someone forgetting to use the new type) falls back to infrastructure, the safe default, rather than silently landing in source-unsupported by accident. Made this explicit in SourceNotConforming's docstring so it's discoverable at the raise site, not just in the classifier.

    Test that fails if a refusal regresses to infrastructure: tests/test_ingest_ledger.py has one test per refusal category (unsupported compressed-image format, non-conforming pass-through video, raw-image schema, derived-topic collision) that triggers the real raise site — not a synthetic exception — and asserts classify_ingest_failure(caught_error) == IngestFailureKind.SOURCE_UNSUPPORTED. If any of those raise sites reverted to a bare ValueError, the exception type check would fail and so would the classification assertion. The existing three kinds are covered by their own tests in the same file (SourceNotFound → source-missing, McapError → source-unreadable, unrecognized → infrastructure) and are unchanged.

    PR is #288 (draft), ready for review.

  6. kstonekuan commented on Aug 31, 2026

    @kstonekuan
    ContributorAuthor

    Resolved by #288.

  7. added a commit that references this issue on Aug 31, 2026
    eff4347
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

advancedNeeds codebase familiarity; not a starter issuebugSomething isn't workinghelp wantedExtra attention is needed

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions