Add post-import processing lifecycle to Finding (processing_status) (1/3)#15150
Add post-import processing lifecycle to Finding (processing_status) (1/3)#15150devGregA wants to merge 3 commits into
Conversation
Findings now carry an explicit post-import processing state: importers create findings as "pending" and post_process_findings_batch stamps "processed" on completion or "failed" on error (the exception still propagates). Findings created outside the import pipeline (manual entry, API) default to "processed" and never enter "pending". This makes silently-dying post-processing (worker OOM, task crash) visible on the findings themselves, and defines the post-import pipeline (dedupe, false-positive history, issue updater, grading, JIRA push) as a single terminating chain — groundwork for pending/failed counts on import pages and stuck-finding health checks. Performance: - column default "processed": metadata-only ALTER on PostgreSQL 11+, no backfill; all pre-existing findings read correctly - stamping is one bulk UPDATE per existing 1000-finding batch; no signals, no per-row saves - partial index covers only the small, hot PENDING working set - pghistory finding-history triggers regenerated for the new columns API: fields exposed read-only on the Finding API, plus a ?processing_status= filter. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
post_process_findings_batch now ends with one bulk UPDATE stamping the batch's processing lifecycle, so every import/reimport step that runs a post-processing batch costs exactly one additional query. Adjust the perf baselines accordingly (+1 per step; steps with no batch, like empty-report reimports, are unchanged). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
34a5779 to
ec8eb15
Compare
|
Reviewed & tested — approving (milestone 3.1.100). Validated against a running instance:
Also confirmed via review: the |
Maffooch
left a comment
There was a problem hiding this comment.
Really like the direction here — making post-processing state observable is overdue. One correctness gap in the failed semantics, plus a follow-on suggestion.
failed won't actually fire on a JIRA push failure. In post_process_findings_batch, the JIRA step is jira_services.push(object_to_push) inside the try, but add_jira_issue/update_jira_issue catch every exception internally and return a (success, message) tuple rather than raising (see dojo/jira/helper.py:905 / :1070 — hard failures go through failure_to_add_message → return False, message). So:
- No exception propagates → the
try/exceptnever trips → thefinallystampsprocessedeven when the ticket failed to create. - This holds in every execution mode, including the synchronous
block_execution=True/deduplication_execution_mode=syncpath — it's not just an async-dispatch artifact. In the default async mode the push is also a separate fire-and-forget task, sopush()returns anAsyncResultthe batch couldn't inspect even if it wanted to.
Net: processing_status = processed currently means "the batch reached the JIRA-push step," not "the ticket was pushed." Worth tightening since the PR explicitly lists JIRA push as part of the terminating chain.
Requested change (per-finding, keep JIRA async): have the push task (push_finding_to_jira / push_finding_group_to_jira) record its own outcome — when it returns False, stamp that finding (or each finding in the group) processing_status = failed. That keeps the async throughput/retry isolation while making the failed state truthful. Two things to handle:
- Ordering: the batch
finallystampsprocessedafter dispatching the async push, so the task must be the last writer. Fine in normal async execution (task runs later), but watch the eager/force_syncpath where the push runs inline before thefinally— there thefinallywould overwritefailedback toprocessed. Might need the batch to skip re-stamping findings the push already marked, or have the push write last unconditionally. - Groups:
object_to_pushcan be aFinding_Group; fan the status back out to its member findings.
Follow-on suggestion — capture the why, not just the that. Since the push already has the message string from (False, message), consider adding a nullable processing_error (TextField) alongside processing_status, populated on failed. Today that reason only lands in log_jira_alert; surfacing it on the finding makes the failed working-set self-service to triage and pairs well with the "pending/failed counts on import pages" groundwork you mention. Happy to spin this out as a separate issue if you'd rather keep this PR tight.
There was a problem hiding this comment.
This was on my list to implement as well :-) The idea I had is that the "processing complete" would/could/should be set after all post processing is complete, not only deduplication (using the new option from #15007)
For example pushing to JIRA can take a while to complete. The dedupe itself is so fast nowadays that in a non-overloaded instance the time difference between finding creating and processed_at is going to be very small. In earlier conversations we considered multiple flags, or a json field with multiple flags. i.e. deduplication_status, jira_delivery_status (or external issue status, or github status, ...)
An easy escape here to make my comment not blocking the pr/feature would be to rename procesing_statuts to deduplication_status (and the timestamp field in a similar way)
Per review: add_jira_issue/update_jira_issue swallow errors internally and report them as a (success, message) tuple, so a failed push never raised and the batch stamped processed even when the ticket was never created — in every execution mode. The push tasks are now the writer of record for the JIRA outcome (record_finding_push_outcome in dojo/jira/helper.py): - a (False, message) result stamps the finding failed with the helper's message in the new processing_error field; group pushes fan the outcome out to member findings, and the group task's separate-ticket updates are stamped individually by their own results; - a later successful push heals failed back to processed and clears the error; successful pushes never touch pending findings — the batch owns the happy-path stamp. Ordering: the batch's final bulk UPDATE excludes failed rows, so an inline (eager/force_sync) push failure stamped inside the try block survives the finally, while in async execution the push task simply runs after the batch and is the last writer. The batch's own exception path now records str(e) in processing_error before re-raising. A finding stamped failed by a push deliberately stays failed through later non-pushing batches until a push succeeds — the failure is unresolved until then. Also corrects the (bool, str) return annotations on the five push/issue helpers (they were declared tuple[str, bool], which had already misled the finding-group perf test's mock — updated to return a real tuple now that the tasks unpack it). Migration 0279 adds processing_error (blank text, editable=False → read-only in the API) and regenerates the pghistory triggers. Tests: 6 new in unittests/test_processing_status.py — failure stamping with reason, heal on success, pending untouched by successful pushes, group fan-out, eager-ordering (real dispatch path under CELERY_TASK_ALWAYS_EAGER), and batch exception reason capture. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
@Maffooch Thank you for the precise diagnosis — you were right on all three counts (the internal Push task as writer of record —
Ordering — handled exactly as you suggested: the batch's final bulk UPDATE now excludes
One deliberate semantic worth flagging: a finding stamped 7 new tests in Coordination note: this adds migration |
|
@valentijnscholten Great to hear it was on your list too — and your comment sharpened the naming question nicely. With Cody's requested change now implemented (the JIRA push tasks stamp their own per-finding outcome, including the failure reason in a new On the multi-flag / per-stage design ( Re: #15007 interplay — the batch remains the join point for |
This PR and #15152 both update the pinned query-count baselines in
unittests/test_importers_performance.pyandunittests/test_tag_inheritance_perf.py, each measured againstdevwithout the other. They are functionally independent and can merge in either order — but whichever merges second needs a rebase with re-measured baselines, because the deltas add up. Example:EXPECTED_ZAP_IMPORT_V2is 287 on dev, 288 in each PR alone, and 289 once both are in (+1 for this PR's batch stamp, +1 for #15152's created-events batch). I'll push the combined-baseline rebase on whichever PR lands second.Add post-import processing lifecycle to Finding (
processing_status)What
Findings now carry an explicit post-import processing state:
processing_status:pending/processed/failed(TextChoices, mirroring thestatus-enum pattern used elsewhere in the platform)
processed_at: when post-processing last completedImporters create findings as
pending;post_process_findings_batch(dedupe → false-positivehistory → issue updater → grading → JIRA push) stamps
processedon completion orfailedonerror — the exception still propagates, but the failure is now visible on the findings
themselves instead of dying silently in a worker. Findings created outside the import pipeline
(manual entry, API) default to
processedand never enterpending.Why
Support keeps fielding variants of "the import ran but nothing happened / findings are stuck /
close-old didn't fire" where post-processing died invisibly (worker OOM, task crash) and nothing
in the product showed it. This is the smallest primitive that makes pipeline state observable,
and it defines the boundary of "post-processing" as one terminating chain — groundwork for
surfacing pending/failed counts on import pages and health alerts for stuck findings.
Performance notes
existing rows correctly read
processed.UPDATEper batch (same 1,000-finding batches the importeralready uses) — no signals, no per-row saves.
WHERE processing_status = 'pending'): only the small, hot pendingworking set is indexed; steady-state instances pay near-zero index maintenance.
pending(no dashboard flicker mid-scan).
existing triggers (same write class as dedupe's own updates). The migration regenerates the
history triggers to include the new columns — migration auto-generated by
makemigrations.API
processing_statusandprocessed_atappear on the Finding API (read-only)./api/v2/findings/?processing_status=pending|processed|failed.Tests
unittests/test_processing_status.py:processedwithprocessed_atsetfailed(and still raises)processed🤖 Generated with Claude Code