Skip to content

test(finding): accept either deadlock victim in the bulk-delete vs tag writer race - #16100

Merged
Maffooch merged 1 commit into
bugfixfrom
fix/bulk-delete-m2m-race-test-victim
Sep 26, 2026
Merged

Maffooch merged 1 commit into
bugfixfrom
fix/bulk-delete-m2m-race-test-victim

Conversation

@Maffooch

@Maffooch Maffooch commented Sep 26, 2026 •

Copy link
Copy Markdown
Contributor

[sc-15987]

Description

TestBulkDeleteRacesConcurrentM2MWriter.test_tag_writer_holding_a_tag_row_does_not_fail_the_chunk (added in #16091) is flaky. Its failure is OperationalError: deadlock detected, raised in the concurrent tag writer thread.

The test forces a real deadlock. bulk_add_tags_to_instances holds a tag's count row, and the bulk-delete chunk holds the finding rows and waits on that tag row. The writer then commits, and its deferred foreign-key check waits on the chunk's finding lock. The test assumed Postgres always aborts the chunk, whose conflict retry then finishes the delete.

Postgres does not guarantee which side it aborts. A waiting backend runs the deadlock check once, deadlock_timeout after it started waiting, and aborts itself if it finds a cycle. The chunk starts waiting first, so it is the victim only when the writer reaches its COMMIT within deadlock_timeout. On a slow or loaded machine the writer gets there later: the chunk's one check has already run and found no cycle, so the writer's check finds the cycle and the writer is aborted.

Both orders meet the guarantee #16091 describes: the chunk deletes, no tag through row survives, and the tag count stays consistent. The product code needs no change. When the writer is the victim its tag add rolls back and the chunk commits on its first attempt, which is also a consistent result.

Changes, all in unittests/test_dedupe_delete_commit_race.py:

  • A shared helper sets up the cycle. It then asserts the delete outcome for both orders, and that exactly one side was the deadlock victim. It counts the chunk's attempts through lock_findings_for_delete and checks the writer for a transient conflict error. Any other writer error still fails the test.
  • The existing test runs the helper with no timing control and accepts either victim.
  • A new test, test_tag_writer_aborted_as_the_deadlock_victim_leaves_the_chunk_to_finish, forces the writer-victim order. The writer holds its COMMIT for twice the server's deadlock_timeout, which is read at run time. The test then asserts the writer was aborted and the chunk did not retry. So the order that used to be the flake is covered on purpose.

The follow-up that #16091 mentions still applies: bulk_add_tags_to_instances could take FOR KEY SHARE on its findings before the tag UPDATE and remove this cycle entirely.

Test results

These ran in the OSS unit-test compose (ossfix-unittests image) with --keepdb:

  • Before, 50 runs of the unchanged test: 47 passed, 3 failed with OperationalError: deadlock detected in the writer.
  • After, 50 runs of both tag-writer tests: 50 passed, 0 failed.
  • unittests.test_dedupe_delete_commit_race: 9 tests OK, with DD_V3_FEATURE_LOCATIONS both off and on.
  • ruff check --config ruff.toml with ruff 0.16.5 (the pinned version) passes.

Documentation

Test only, no user-facing change.

🤖 Generated with Claude Code

…g writer race

test_tag_writer_holding_a_tag_row_does_not_fail_the_chunk assumed Postgres
always aborts the bulk-delete chunk when it deadlocks with
bulk_add_tags_to_instances. Postgres does not promise that. A waiting
backend runs the deadlock check once, deadlock_timeout after it started
waiting. The chunk starts waiting first, so it is the victim only when the
writer reaches its COMMIT within deadlock_timeout. On a slow or loaded
machine the chunk's one check runs before the cycle exists, and the writer
is aborted instead. That failed the test about 1 run in 17 locally
(3 of 50).

Both orders satisfy what #16091 guarantees: the chunk deletes, no tag
through row survives, and the tag count stays consistent. The test now
asserts exactly that, plus that exactly one side was the deadlock victim
(the chunk's attempts are counted through lock_findings_for_delete). A new
test forces the writer-victim order deterministically by holding the
writer's COMMIT past the chunk's deadlock check, so both orders stay
covered.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Maffooch Maffooch added this to the 3.3.300 milestone Sep 26, 2026
@Maffooch
Maffooch enabled auto-merge September 26, 2026 00:16
@Maffooch
Maffooch added this pull request to the merge queue Sep 26, 2026
Merged via the queue into bugfix with commit 7335671 Sep 26, 2026
48 checks passed
@Maffooch
Maffooch deleted the fix/bulk-delete-m2m-race-test-victim branch September 26, 2026 00:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants