User-dataset collection + admission gatekeeper - #139
Merged
Conversation
When a user runs inference we can keep their CT + our segmentation to grow an
in-house dataset -- but only if it clears an admission gate. Fully decoupled from
the user experience: runs on a daemon thread AFTER the result is delivered, every
failure is swallowed, and it's a no-op unless USER_DATASET_PATH is set (so merging
changes nothing until switched on).
Gatekeeper (services/user_dataset.py), four gates:
1. real CT? valid 3D volume, plausible dims, CT-like HU range (air + tissue)
2. duplicate? exact (sha256) + near-duplicate (downsampled perceptual) vs registry
3. usable? our own segmentation found plausible organs (voxel + organ counts)
4. abuse? rolling 24h per-user / per-IP / global quotas + size cap -> a
competitor dumping thousands of files can't flood the dataset
Accepted scans are promoted into a PanTS-mirroring layout under USER_DATASET_PATH
(image_only/USER_########/ct.nii.gz, mask_only/USER_########/combined_labels.nii.gz
+ segmentations/<organ>.nii.gz + metadata.json), so it can sit beside PanTS once a
writable location is granted. Rejections are logged to rejections.jsonl (audited,
not silently dropped). File-based registry (atomic writes) -- no DB migration.
Hook: api_blueprint fires collect_user_scan_async after the job is marked
completed; client IP is captured in the request context for the per-IP quota.
Tests: tests/unit/test_user_dataset.py (quotas, dedup, CT/segmentation validity,
end-to-end promotion). Added to CI (+nibabel to the backend test deps).
Constant/2D volumes compress below the size floor, so they're rejected as too_small before the constant/not_3d checks. Adjust the constant-volume assertion and give the 2D case enough entropy to reach the dimensionality gate. 8/8 pass.
Addresses a subagent review of the gatekeeper: - CRITICAL (C1): the size cap was on the COMPRESSED .nii.gz, so a crafted header declaring e.g. 2048^3 could decode to ~34 GB and OOM-kill the worker (breaking the 'never affect the user' guarantee). Now bound the DECODED voxel count (MAX_VOXELS, default 400M) before any np.asarray, and load native dtype (int16) instead of forcing float32. Also require all values finite (reject +/-inf). - H2: promote into <case>.partial staging dirs and publish with atomic os.replace, so a partial failure never leaves a half-written case the next admission merges into. next_id is committed only after the files are published. - H4: run the expensive gates (validate/fingerprint/quality) OUTSIDE the registry lock; hold it only for dedup+reserve+promote+commit. Record an attempt event even on rejection, so a flood of REJECTED garbage is rate-limited too. - H3: attribute the per-IP quota to request.remote_addr (trusted proxy) instead of the client-spoofable left-most X-Forwarded-For. - H1: add a cross-process fcntl file lock around the registry critical section (threads-only fallback on Windows/CI), so it stays correct if workers ever > 1. - M2: write uint8 sublabels with a fresh header (affine only, explicit dtype) so the label map's scl_slope/datatype can't misencode the binary mask. - L3: skip collection when the input isn't a CT file (ShapeKit passes a dir). Tests: +no-op-when-unset, +voxel-guard (anti-OOM), +non-finite rejection. 11/11 pass.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Collects users' CT scans + our segmentation into an in-house dataset — but only what's worth keeping, via an admission gatekeeper. Completes the data-saving feature and the "thing that decides what goes in and what doesn't."
Design principle: never touches the user
It runs on a daemon thread AFTER the result is already delivered, every failure is swallowed, and it's a no-op unless
USER_DATASET_PATHis set. A user who uploads garbage still gets their result — the garbage just doesn't enter the dataset. Merging this changes nothing until it's switched on in the environment.The gatekeeper (
services/user_dataset.py) — four gatessha256of the file) and near-duplicate (downsampled, quantized perceptual hash) vs a registry — re-uploads that differ only in compression/metadata are caught.What accepted scans become (PanTS-mirroring layout)
So it can sit beside PanTS/CancerVerse once a writable location is granted. Uses a file-based registry with atomic writes — no DB migration.
Wiring
api_blueprint.pyfirescollect_user_scan_async(...)right after a job is markedcompleted; client IP is captured in the request context (the worker thread has none) for the per-IP quota.constants.py: documentedUSER_DATASET_PATH.Tests / CI
tests/unit/test_user_dataset.py: quotas (user/IP/global + pruning), dedup, registry round-trip, CT validity (accepts a real CT, rejects constant / non-CT / 2D), segmentation-quality gate, and end-to-end promotion (accepted → full layout; duplicate → logged, not stored).nibabelto its deps).compileallcovers the module too.To switch on (deploy)
Set
USER_DATASET_PATHto a writable staging dir (e.g./home/visitor/UserData) and restart. Tune quotas viaUSER_DATASET_DAILY_PER_USER/_PER_IP/_GLOBAL,USER_DATASET_MAX_CT_BYTES,USER_DATASET_MIN_ORGAN_VOXELS.