Skip to content

fix(finding): retry deferred-FK race on single-finding delete - #15748

Merged
Maffooch merged 1 commit into
bugfixfrom
cmm/amazing-volta-0w1tsj
Aug 20, 2026
Merged

fix(finding): retry deferred-FK race on single-finding delete#15748
Maffooch merged 1 commit into
bugfixfrom
cmm/amazing-volta-0w1tsj

Conversation

@Maffooch

Copy link
Copy Markdown
Contributor

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:

DETAIL: Key (id)=(<finding_id>) is still referenced from table "dojo_test_import_finding_action".

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_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. 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_conflict in dojo/utils.py, retrying on FK-violation 23503 and on deadlock/serialization 40P01/40001). The synchronous single-finding delete path had no such protection.

This PR adds delete_finding_with_conflict_retry(finding, **kwargs) in dojo/finding/helper.py, which re-runs Finding.delete() on a transient conflict with exponential backoff and re-raises anything else (e.g. a unique-violation 23505) or a conflict that survives every attempt. Each attempt runs in a fresh transaction (there is no ATOMIC_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 (mocks Finding.delete, so no DB needed), covering:

  • success on first attempt deletes once, no sleep;
  • FK-conflict (23503) retried, then succeeds;
  • deadlock / serialization-failure (40P01 / 40001) retried, then succeeds;
  • unique-violation (23505) is not retried and surfaces immediately;
  • a conflict that survives every attempt is re-raised;
  • retry backoff grows with each attempt.

The SQLSTATE detection and retry control-flow were also validated against real psycopg.errors exception shapes.

Documentation

None needed — internal reliability fix, no user-facing behavior or settings change.

Checklist

  • Bugfix submitted against the bugfix branch.
  • Ruff compliant.
  • Added unit tests.
  • No model changes / no migration required.

🤖 Generated with Claude Code


Generated by Claude Code

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
@Maffooch
Maffooch requested a review from blakeaowens as a code owner August 20, 2026 12:20
@Maffooch Maffooch added this to the 3.2.300 milestone Aug 20, 2026 — with Claude
@Maffooch Maffooch added bugfix and removed unittests labels Aug 20, 2026 — with Claude
@Maffooch
Maffooch added this pull request to the merge queue Aug 20, 2026
Merged via the queue into bugfix with commit a73dc0f Aug 20, 2026
48 checks passed
@Maffooch
Maffooch deleted the cmm/amazing-volta-0w1tsj branch August 20, 2026 17:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants