fix(finding): retry deferred-FK race on single-finding delete - #15748
Merged
Conversation
Deleting a single finding via the API (DELETE /api/v2/findings/{id}/) or the UI
delete view runs Django's collector, which clears the finding's
Test_Import_Finding_Action children before deleting the finding row. Those FK
constraints are DEFERRABLE INITIALLY DEFERRED, so a concurrent import that commits
a new child row referencing the finding between the child clear and the
transaction COMMIT trips a foreign-key violation (SQLSTATE 23503) at commit time,
surfaced to the caller as an Internal Server Error.
The async cascade delete already retries this delete-vs-import race
(_is_retryable_delete_conflict), but the synchronous single-finding path had none.
Add delete_finding_with_conflict_retry, which re-runs the delete on a transient
conflict (FK violation 23503, or deadlock/serialization 40P01/40001) with
exponential backoff and re-raises anything else or a conflict that survives every
attempt. Wire it into both the API viewset and the UI delete view.
Tests cover: success first attempt, FK-conflict retried then succeeds,
deadlock/serialization retried, unique-violation not retried, retries exhausted
re-raises, and growing backoff.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AFdaF25N1sK1Pfvb2xr65q
blakeaowens
approved these changes
Aug 20, 2026
devGregA
approved these changes
Aug 20, 2026
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.
Description
Deleting a single finding via the API (
DELETE /api/v2/findings/{id}/) or the UI delete view intermittently returns an Internal Server Error under load, with:raised at transaction COMMIT time.
Root cause is a delete-vs-import race. A single-finding delete runs Django's collector, which clears the finding's
Test_Import_Finding_Actionchildren before deleting the finding row. Those FK constraints areDEFERRABLE INITIALLY DEFERRED, so a concurrent import that commits a new child row referencing the finding between the child clear and the transaction COMMIT trips a foreign-key violation (SQLSTATE23503) at commit time. The reference is real but transient: on a re-run the collector clears the newly-created child and the delete completes.The async cascade delete already handles exactly this race (
_is_retryable_delete_conflictindojo/utils.py, retrying on FK-violation23503and on deadlock/serialization40P01/40001). The synchronous single-finding delete path had no such protection.This PR adds
delete_finding_with_conflict_retry(finding, **kwargs)indojo/finding/helper.py, which re-runsFinding.delete()on a transient conflict with exponential backoff and re-raises anything else (e.g. a unique-violation23505) or a conflict that survives every attempt. Each attempt runs in a fresh transaction (there is noATOMIC_REQUESTS), and the failed attempt has already rolled back, so the finding still exists to be re-deleted. It is wired into both the API viewset (dojo/finding/api/views.py) and the UI delete view (dojo/finding/ui/views.py).No behavior change on the happy path; no schema/model change, so no migration.
Test results
Added
unittests/test_finding_delete_conflict_retry.py(mocksFinding.delete, so no DB needed), covering:23503) retried, then succeeds;40P01/40001) retried, then succeeds;23505) is not retried and surfaces immediately;The SQLSTATE detection and retry control-flow were also validated against real
psycopg.errorsexception shapes.Documentation
None needed — internal reliability fix, no user-facing behavior or settings change.
Checklist
bugfixbranch.🤖 Generated with Claude Code
Generated by Claude Code