Skip to content

feat(#203-A3): ingestor writes file-bearing assets under ds_<hex> - #437

Merged
LukasWodka merged 2 commits into
developfrom
feat/203-a3-ingestor-file-handle
Aug 3, 2026
Merged

feat(#203-A3): ingestor writes file-bearing assets under ds_<hex>#437
LukasWodka merged 2 commits into
developfrom
feat/203-a3-ingestor-file-handle

Conversation

@LukasWodka

@LukasWodka LukasWodka commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator

#203 Track A3 — RFC-0003 D9 phase 2 / D16

Today's #255 (scoped mount) and #569 (engine path) both re-keyed dataset access onto the physical ds_<hex> handle. But the ingestor still hard-refused file-bearing categories under PER_INGESTION_TABLES, because their assets landed in the shared label tree (STORAGE_PATH/<label>) where a same-label re-ingest would overwrite an earlier dataset's files. This PR moves the file tree onto the same handle and lifts the refusal.

Change

  • config.pyDEST_PATH now keys on a new DEST_TABLE (defaults to the TABLE_NAME label, injectable via set_dest_table). This is the one knob: every file-copy primitive, the text profiler, the duplicate validator, and the source-reclaim path already read DEST_PATH off the run config, so they all follow automatically.
  • ingestors/base.py — when per_ingestion_tables, inject the ds_<hex> handle: config.set_dest_table(physical_table_name). File-bearing assets now land in STORAGE_PATH/ds_<hex>/, matching the scoped mount (refactor(P3b): move validator factories into the ModalityRegistry #255) and get_dataset_path (#569). The file-bearing refusal is removed.
  • tests — flipped the "refuses file-bearing" test to assert the handle injection; added a real-Config DEST_PATH test. Flag off is byte-for-byte unchanged (DEST_TABLE = label, set_dest_table never called).

Why it matters

Unlocks image / object-detection / semantic-segmentation / keypoint datasets (the bulk of tracebloc's use-cases) under per-ingestion isolation, and completes the D9-phase-2 file half that #255 + #569 were built to enable. It's the ingestor change the on-HOLD client#490 v0.8.0 image is waiting on.

Scope note

The file table lock intentionally stays keyed on the label — that's pre-existing #408 behavior (the row store already moved to ds_<hex> while the lock stayed on the label); it's a concurrency guard, not a data-isolation unit, so it's out of scope here.

Verified

Full ingestor suite 1903 passed, 1 xfailed; black clean.

Targets develop.

🤖 Generated with Claude Code


Note

Medium Risk
Changes on-disk layout for file-bearing datasets when PER_INGESTION_TABLES is on; misalignment with scoped mounts or backend path resolution could strand or mis-associate assets, though flag-off behavior is unchanged.

Overview
With PER_INGESTION_TABLES enabled, file-bearing ingests no longer error at construction; instead the run repoints the on-disk file tree from the shared label directory to the same ds_<hex> handle used for the MySQL row store.

Config introduces DEST_TABLE (defaults to the user label) and set_dest_table, so DEST_PATH becomes STORAGE_PATH/<DEST_TABLE>. BaseIngestor calls set_dest_table(physical_table_name) when the flag is on, which pulls file copy, text profiling, duplicate checks, and source reclaim along without separate changes. TABLE_NAME and flag-off behavior stay as today.

Tests now assert handle injection for image-classification under the flag and DEST_PATH repointing; package version 0.8.0 → 0.8.1.

Reviewed by Cursor Bugbot for commit 110d3d8. Bugbot is set up for automated code reviews on this repo. Configure here.

D9 phase 2 / D16. #255 (mount) + #569 (engine path) both re-keyed onto the
physical ds_<hex> handle; the ingestor still hard-refused file-bearing
categories under PER_INGESTION_TABLES because their assets landed in the shared
label tree (STORAGE_PATH/<label>), where a same-label re-ingest would overwrite
an earlier dataset's files. This lifts the refusal by moving the file tree onto
the same handle.

- config: DEST_PATH now keys on a new DEST_TABLE (default = TABLE_NAME label,
  injectable via set_dest_table). One knob — every file-copy primitive, the
  text profiler, the dup validator, and the reclaim path already read DEST_PATH
  off the run config, so they all follow.
- base: when per_ingestion_tables, inject the ds_<hex> handle via
  config.set_dest_table(physical_table_name), so file-bearing assets land in
  STORAGE_PATH/ds_<hex>/ (matching the scoped mount + get_dataset_path). Remove
  the file-bearing refusal.
- tests: flip the "refuses file-bearing" test to assert the handle injection;
  add a real-Config DEST_PATH test. Flag off is byte-for-byte unchanged.

Unblocks image/detection/segmentation/keypoint datasets under per-ingestion
isolation, and the on-HOLD client#490. Full suite 1903 passed; black clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@LukasWodka LukasWodka self-assigned this Aug 3, 2026
@LukasWodka

Copy link
Copy Markdown
Collaborator Author

bugbot run

The package-change version guard (data-ingestors#420) requires a bump when
tracebloc_ingestor/ changes. A3 completes the per-ingestion file path started
in 0.8.0 (#408); this is the image client#490 pins.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@LukasWodka

Copy link
Copy Markdown
Collaborator Author

bugbot run

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 110d3d8. Configure here.

@saqlainsyed007 saqlainsyed007 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: APPROVE.

Reviewed the full-file context, not just the hunks. The change is correct and consistent with the row-store side of RFC-0003 D9 phase 2.

What I verified:

  • DEST_PATH repointDEST_TABLE defaults to TABLE_NAME, so flag-off is byte-for-byte unchanged; set_dest_table(physical_table_name) is the only writer, and every consumer (file_transfer copy primitives, text_profile, duplicate_validator, reclaim_source) reads DEST_PATH off the same run config, so the single knob genuinely propagates.
  • Path safetyphysical_table_name is always ds_<uuid4().hex> (hex-only), so no traversal/injection surface is introduced by moving the file tree.
  • Reclaim_reclaim_source still keys the source gate on TABLE_NAME (the CLI's .tracebloc-staging/<label> dir) and only uses DEST_PATH for the overlap backstop; staging (<label>) and dest (ds_<hex>) don't overlap, so reclaim still fires correctly.
  • Table lock — stays on the label via TableLock(self.table_name, …), matching the scope note; concurrency-only, not a data-isolation unit.
  • Refusal removal — clean: _FILE_BEARING_CATEGORIES is still used (base.py:547, 1031), no dead import.
  • Version bump — 0.8.0→0.8.1 is single-sourced in __init__.py and parsed by setup.py:_read_version; nothing else to update.
  • Tests — real assertions (handle injection via set_dest_table.assert_called_once_with, and a real-Config DEST_PATH repoint asserting label untouched), not vacuous.

Non-blocking observation (no change requested): set_dest_table permanently mutates the shared Config override with no reset path. This is safe because cli/run.py builds one Config → one ingestor per process; if a future caller ever reused one Config across a flag-on then flag-off ingestor, the stale ds_<hex> DEST_TABLE would leak into the flag-off run. Worth a one-line comment noting the single-run-per-config invariant, but not reachable today.

@LukasWodka
LukasWodka merged commit 66946ae into develop Aug 3, 2026
13 checks passed
@LukasWodka
LukasWodka deleted the feat/203-a3-ingestor-file-handle branch August 3, 2026 12:55
LukasWodka added a commit that referenced this pull request Aug 3, 2026
…st (data-ingestors#439) (#440)

* fix(#203-A3): reclaim the ds_<hex> file tree on a failed per-ingestion ingest

data-ingestors#439 (Bugbot Medium on the #437 promotion): a failed file-bearing
ingest under PER_INGESTION_TABLES leaves assets under its unique ds_<hex>
handle. Unlike the flag-off label tree — which a same-label re-run overwrites —
the retry mints a FRESH handle, so those files are never overwritten and nothing
reclaims them: a lasting PVC leak for image-sized payloads. (Dormant in prod
today only because the flag defaults off; a blocker to close before enablement.)

- file_transfer.reclaim_dest_tree() — guarded, best-effort rmtree of DEST_PATH.
  SAFETY (opt-in, provable): removes it ONLY when the basename is a ds_<hex>
  handle (never a user label) AND it resolves to a direct child of STORAGE_PATH;
  realpath before check + delete; any error logged + swallowed so it can't mask
  the ingest's original error. Mirrors reclaim_source's safety posture.
- base.py failure handler calls it for per-ingestion file-bearing runs (NOT
  gated on inserted_records — files can land before any row). Flag-off is a safe
  no-op (the handle guard only matches ds_<hex>).
- +4 tests (removes handle tree; leaves flag-off label tree; refuses a
  non-direct-child handle; no-op on missing dest).

SCOPE: this closes the GRACEFUL-failure path. Hard kills (OOMKilled/SIGKILL)
bypass the except-branch entirely — those orphaned file trees are the edge-side
husk sweep's job (client-runtime, like the DB husk tables); tracked separately.

Full suite 1907 passed; black clean. __version__ 0.8.1 -> 0.8.2.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(#439): gate dest-tree reclaim on `not dataset_registered` (Bugbot High)

Bugbot: reclaim_dest_tree ran on every graceful per-ingestion file-bearing
failure without checking dataset_registered. A LATE failure after
send_ingest_summary succeeded (the case the row compensating-delete already
guards) would rmtree the REGISTERED dataset's ds_<hex> tree while the backend
still points at it — permanent asset loss.

- Add `and not dataset_registered` to the reclaim condition, mirroring the row
  compensating-delete guard: only a genuinely-unregistered failed run's files
  are reclaimed; a registered dataset's files are never touched.
- +2 tests: unregistered failure reclaims the dest tree; late failure after
  registration does NOT (mutation check — dropping the guard fails it).

Full suite green; black clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
@LukasWodka

Copy link
Copy Markdown
Collaborator Author

/fr-pass

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants