Skip to content

fix(locations): tolerate concurrent creation in bulk_get_or_create - #15845

Merged
Maffooch merged 1 commit into
bugfixfrom
cmm/friendly-edison-abcxgz
Sep 4, 2026
Merged

fix(locations): tolerate concurrent creation in bulk_get_or_create#15845
Maffooch merged 1 commit into
bugfixfrom
cmm/friendly-edison-abcxgz

Conversation

@Maffooch

@Maffooch Maffooch commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Description

A scan import could abort with a database IntegrityError when a second import created the same location row concurrently.

AbstractLocation.bulk_get_or_create looks up existing rows by identity_hash, then bulk-inserts the ones it didn't find. identity_hash is a global singleton with a unique constraint, so when two imports reference the same package/endpoint, one import can commit the row in the window between the other's existence check and its bulk_create. The second import's subtype bulk_create then raised:

duplicate key value violates unique constraint "<subtype>_identity_hash_key"
DETAIL:  Key (identity_hash)=(<hash>) already exists.

That exception propagated out of the location manager's persist() and failed the entire async scan-import task. The previous code deliberately let the transaction roll back on this race, which is exactly what turned a routine collision into a failed import.

Fix

Create the parent Location rows and their subtype rows together inside a savepoint. On a unique-constraint collision the savepoint rolls back — parent Location rows included, so nothing is orphaned — and the enclosing transaction stays usable. The code then re-resolves the rows the concurrent writer committed, carries over their association data, and retries only the rows still missing (bounded retry). A routine collision no longer aborts the import; it resolves to the existing row and continues.

No schema change and no migration: the fix is purely in the create/recover logic, and the existing unique constraint is what makes the recovery correct.

This lives on AbstractLocation, so every location subtype (URL and the Pro Dependency/CodeLocation companions that inherit it) gets the same race tolerance. Pro does not override bulk_get_or_create, so no companion change is required for this path.

Test results

Added a regression test in unittests/test_bulk_locations.py that reproduces the race deterministically: it hides a pre-existing row from the first existence lookup only, so the code attempts a duplicate INSERT and hits the real database unique constraint, then asserts recovery — the raced row resolves to the original (no duplicate created), the genuinely-new row in the same batch is still created, and no orphaned parent Location rows are left behind. The test fails on the previous code (uncaught IntegrityError) and passes with the fix.

Note: this environment has no Docker daemon or Postgres, so the suite was validated through CI rather than locally.

Documentation

No user-facing behavior or documentation change; internal import-robustness fix only.

Checklist

  • Bugfix submitted against the bugfix branch.
  • Added a regression test to the unit tests.
  • No model changes and no migration.
  • Ruff compliant.

🤖 Generated with Claude Code

https://claude.ai/code/session_014K826EST31JnjV4xGv6jRf


Generated by Claude Code

Two scan imports that reference the same package or endpoint race to
create the same location row. identity_hash is a global singleton with a
unique constraint, so when one import commits the row between another
import's existence check and its bulk INSERT, the second import's
subtype bulk_create raised IntegrityError (duplicate key value violates
unique constraint "<subtype>_identity_hash_key"). That exception
propagated out of the whole persist() and aborted the entire scan
import, failing the async import task.

The previous code deliberately let the transaction roll back on this
race. Instead, create the parent Location and subtype rows together in a
savepoint; on a unique-constraint collision the savepoint rolls back
(parents included, so no orphaned Location rows), then re-resolve the
rows the concurrent writer committed, carry over their association data,
and retry only the rows still missing. The enclosing transaction stays
usable throughout, so a routine collision no longer aborts the import.

Add a regression test that hides a pre-existing row from the first
existence lookup so the code hits the real DB unique constraint, and
asserts recovery: the raced row resolves to the original (no duplicate),
the genuinely-new row is still created, and no orphaned parent Location
rows are left behind.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014K826EST31JnjV4xGv6jRf
@Maffooch Maffooch added this to the 3.3.100 milestone Sep 2, 2026 — with Claude
@Maffooch Maffooch added bugfix and removed unittests labels Sep 2, 2026 — with Claude
@Maffooch Maffooch modified the milestones: 3.3.100, 3.3.0 Sep 2, 2026
@Maffooch
Maffooch added this pull request to the merge queue Sep 3, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Sep 3, 2026
@Maffooch
Maffooch added this pull request to the merge queue Sep 3, 2026

Maffooch commented Sep 3, 2026

Copy link
Copy Markdown
Contributor Author

Heads-up: this PR was dropped from the merge queue with CI_FAILURE, but the failure is not from this PR's diff — it's a queue-wide flaky suite. Standing down rather than pushing a change (there's nothing in this diff to fix, and no fix to port).

Evidence:

  • This PR's own head (1fd5304) passed all checks, and it passed the merge-queue Unit tests in an earlier batch (gh-readonly-queue/bugfix/pr-15845-416b36ae…). It only failed in the later batch (…-2333ae08…).
  • The two failing jobs are both test-rest-framework, and neither touches this PR's changed files (dojo/location/models.py, unittests/test_bulk_locations.py):
    • test_asset_edit_cannot_add_authorized_users (unittests/test_v3_alias_authorized_users_api_authz.py) — AssertionError: 403 != 404 "No Product matches the given query" (the expected product row wasn't present — test-isolation/ordering).
    • test_watson_async_search_index — a request 500s and the failure surfaces in dojo/notifications/context_processors.py ('NoneType' object has no attribute 'is_authenticated' on an anonymous request).
  • The same merge-queue Unit tests failure is hitting unrelated PRs batched in the same window (e.g. fix(login): render login page when the Site row for SITE_ID is missing #15843, fix(fortify): only flag suppressed FPR findings as false_p (reimport closing all findings) #15839, chore(deps): bump browserslist from 4.28.1 to 4.28.8 in /docs #15849) — a dependency bump and parser/login fixes that don't go near locations or these tests.

This change only adds concurrent-creation recovery to AbstractLocation.bulk_get_or_create; it doesn't alter the non-colliding path, and neither failing test exercises a location-hash collision. Re-queuing should get it green; happy to take another look if a re-run reproduces a failure in the changed code.


Generated by Claude Code

Merged via the queue into bugfix with commit 4f2b6d0 Sep 4, 2026
48 checks passed
@Maffooch
Maffooch deleted the cmm/friendly-edison-abcxgz branch September 4, 2026 00:13
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.

4 participants