Repository navigation
Fix null detections in exports & API. Don't mark images as processed too soon - #1312
Merged
Merged
Conversation
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.
Summary
Fixes #1310.
Null detections (empty-bbox sentinels marking "image processed, nothing found") were being created before the downstream save steps inside
save_results. Two consequences:filter_processed_imageswould then skip it on retry, leaving the image permanently stuck as "processed, zero detections." Observed in production where several hundred captures had only null detections and no real ones.create_and_update_occurrences_for_detectionsiterated every detection including nulls, so each null marker spawned anOccurrencewithdetermination=NULL. Those leaked throughOccurrenceQuerySet.valid()(which only excluded occurrences with zero detections, not occurrences whose only detection is a null).Reviewer heads-up — silent semantic change to
OccurrenceQuerySet.valid()valid()changes meaning from "has any detection" to "has at least oneDetection.objects.valid()row ANDdeterminationis not null." Three call sites pick this up without any line change at the call site:OccurrenceViewSet.get_queryset(ami/main/api/views.py) — intended target of the fix. Phantom occurrences stop appearing in the list endpoint.occurrences_count(ami/main/api/views.py) — will silently decrease on any deployment that has accumulated phantoms. No-op on clean deployments.ami/exports/format_types.py) — null-determination occurrences will be excluded from exports. Probably correct (DwC requirestaxonID) but not validated against an actual export run in this PR.If any of those three are load-bearing in a way I'm missing, flag it.
Changes
test(ml)— RED test for the broker-outage path: asserts the null marker is never persisted ifcreate_detection_images.delayraises andfilter_processed_imagesre-yields the image.fix(ml)— move null persistence to the absolute final step insave_results. Null markers now run after thesource_image.save()loop,create_detection_images.delay(),update_calculated_fields_for_events, andDeployment.update_calculated_fields(save=True). Closes the silent-bug window the prior reorder left open.refactor(main)— null-marker abstraction onDetection.Detection.NULL_BBOX = None— canonical sentinel value for new writes.Detection.is_null_markerproperty — recognises bothbbox=Noneand legacybbox=[].Detection.build_null_marker(source_image, detection_algorithm)classmethod — single construction point.DetectionQuerySet.valid()— consumer default (excludes null markers).DetectionQuerySet.null_markers()— narrow, for "has this image been processed?" checks.refactor(main)— sweep inlineNULL_DETECTIONS_FILTERcall sites to the new manager methods acrossami/main/models.py,ami/main/api/views.py,ami/ml/models/pipeline.py, plus anull_detections_q(prefix)helper for relation-prefixed Q expressions.fix(main)— tightenOccurrenceQuerySet.valid()to require at least one valid detection AND a non-null determination. Closes the phantom-Occurrence leak. See the reviewer heads-up above for the consumers that pick up the new semantic.feat(main)—cleanup_null_only_occurrencesmanagement command for per-project cleanup of the field bug. Dry-run by default. Deletes phantom occurrences (no valid detections OR null determination) and dangling null-marker Detection rows on source images that have no real detection. Idempotent.Test plan
test_null_marker_not_persisted_when_broker_dispatch_fails— RED, then GREEN after move-to-end.TestDetectionNullMarker—is_null_markerforNone/[]/ real bbox,build_null_markerfield setup,valid()/null_markers()disjointness.TestOccurrenceValidQuerySet— fixture with real / null-only-detection / null-determination occurrences; assertsvalid()returns only the real, fully-determined one.TestCleanupNullOnlyOccurrencesCommand— dry-run reports without deleting;--commitdeletes phantoms (both the no-real-detection arm and the null-determination arm) while preserving valid rows and null markers on images that also have a real detection; idempotent on second run.ami/main/tests.py+ami/ml/tests.py+ami/jobs/tests/pass locally.Manual e2e (dev deployment)
create_detection_images.delayto raise mid-job. 0 null markers persisted, 0 phantoms; image stays in thefilter_processed_imagesyield list.update_calculated_fields_for_eventsto raise. Same result: 0 null markers persisted.--commitremoved exactly those rows and leftvalid()counts unchanged; a second dry-run reported0 / 0(idempotent). Running it against any affected production deployment is a post-merge ops step.Out of scope — deferred follow-up
transaction.atomic()wrap. The persistence block (real detections → classifications → occurrences → calc-fields → null marker) can still partially commit if a mid-block step raises. This PR closes the ordering window (null marker writing before downstream steps); it does not close the within-block partial-commit window. A narrowtransaction.atomic()wrap withtransaction.on_commitfor celery dispatch is the structural fix, deferred to a separate PR because transaction changes carry concurrency risk (see theselect_for_update+ATOMIC_REQUESTScontention introduced by #1261) and need their own multi-worker e2e.Dual-form
bbox=Nonevsbbox=[]. New writes go throughDetection.NULL_BBOX = None; legacy rows still carrybbox=[]..null_markers()/.is_null_marker/null_detections_q()all recognise both, so no consumer breaks, but the dual form persists until a data migration backfills legacy rows. Worth a follow-up ticket.Re-classification gap. Adjacent:
filter_processed_imagescurrently reprocesses from scratch because there is no mechanism to reclassify existing detections. Worth a separate ticket.Summary by CodeRabbit
New Features
Bug Fixes
Tests