Skip to content

Fix chunk delete deadlock ordering - #2570

Merged
benfrank241 merged 1 commit into
vectorize-io:mainfrom
oldnicke:fix-chunk-delete-deadlock
Jul 7, 2026
Merged

Fix chunk delete deadlock ordering#2570
benfrank241 merged 1 commit into
vectorize-io:mainfrom
oldnicke:fix-chunk-delete-deadlock

Conversation

@oldnicke

@oldnicke oldnicke commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Summary

  • pre-delete memory_links for chunks in deterministic key order before deleting chunks
  • delete chunk rows through an ordered CTE so concurrent chunk removals acquire locks consistently
  • add regression coverage for the ordered delete sequence

Why

Concurrent DELETE FROM chunks ... statements can let PostgreSQL's FK cascade lock overlapping memory_links rows in executor-chosen order. When two writers delete overlapping chunk graphs, that can produce AB/BA row-lock deadlocks. Explicitly deleting the affected links in a stable order removes the unordered cascade as the first lock owner.

Fixes #2560.

Supersedes #2569, which was closed when the fork branch was renamed.

Validation

  • uv run --project hindsight-api-slim pytest -q hindsight-api-slim/tests/test_chunk_storage_delete_ordering.py
  • uv run --project hindsight-api-slim ruff format --check hindsight-api-slim/hindsight_api/engine/retain/chunk_storage.py hindsight-api-slim/tests/test_chunk_storage_delete_ordering.py
  • uv run --project hindsight-api-slim ruff check hindsight-api-slim/hindsight_api/engine/retain/chunk_storage.py hindsight-api-slim/tests/test_chunk_storage_delete_ordering.py

@oldnicke oldnicke changed the title [codex] Fix chunk delete deadlock ordering Fix chunk delete deadlock ordering Jul 5, 2026
@oldnicke
oldnicke marked this pull request as ready for review July 5, 2026 13:09
@benfrank241
benfrank241 merged commit 11da432 into vectorize-io:main Jul 7, 2026
87 checks passed
nicoloboschi added a commit that referenced this pull request Aug 11, 2026
#3387) (#3393)

* fix(retain): match chunk-delete link endpoints through indexable joins (#3387)

The ordered memory_links pre-delete in delete_chunks_by_ids matched link
endpoints with `tu.id = ml.from_unit_id OR tu.id = ml.to_unit_id`. An OR
spanning two columns of ml cannot be driven from either endpoint index, so
PostgreSQL made memory_links the outer relation of a nested-loop semi join
and sequentially scanned the whole table on every delete — O(links x units),
with no bank_id predicate, so every bank scanned every other bank's rows.

Past a few million links that exceeded the asyncpg command timeout and delta
retain failed with a bare TimeoutError (str(TimeoutError()) is empty, so it
logged as "Task execution failed: batch_retain, error:" with nothing after).

Splitting the OR into a UNION of two single-column joins makes each half an
index scan. Measured on PG18 with the current index set, 300k units /
1.5M links, deleting the links of 3 chunks (150 units):

  before  20,420 ms   Seq Scan, 224.9M rows removed by join filter
  after       31 ms   two index scans, same 1,464 rows

10 chunks took 49.8s before — already over the 60s default at a table size
well below production. The row set is identical (EXCEPT in both directions
returns nothing), and the deterministic ORDER BY + FOR UPDATE that #2570
added stay in ordered_links, which still locks the rows in that order.

* test(memories): update store doubles for the per-bank capability API

#3388 moved the store capability to writes_memory_rows_in_sql_for(bank_id) and
added drop_bank_storage, but two duck-typed test doubles still carried the old
surface, so test-api shard 2 has been red on main since it merged:

  test_integrity_violation_not_retried — SimpleNamespace(writes_memory_rows_in_sql=True)
    -> AttributeError: no attribute 'writes_memory_rows_in_sql_for'
  test_list_banks_non_sql_store._NonSqlStore — teardown's delete_bank routes the
    drop through the store for a non-SQL bank
    -> AttributeError: no attribute 'drop_bank_storage'

Both doubles are hand-rolled rather than subclasses of the store base, which is
why the refactor could not update them mechanically.
nicoloboschi added a commit that referenced this pull request Aug 12, 2026
) (#3406)

The deadlock #2570 targeted still fired a few times a day because the
insert-side lock ordering was only partial:

1. _bulk_insert_links sorted on (from, to) — two of the four columns in
   the unique index (from, to, link_type, COALESCE(entity_id, nil)). A
   temporal and a semantic edge on the same pair compared equal, so a
   stable sort left them in input order and concurrent inserts could take
   the two index entries in opposite orders.

2. That order also disagreed with chunk_storage.delete_chunks_by_ids,
   which normalises direction via LEAST/GREATEST. Two different total
   orders can still cycle.

Sort both paths on one canonical key — the full, direction-normalised
unique key — by extracting _lock_order_key and pointing the insert sort
at it. The delete side already uses exactly this order and is unchanged.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Concurrent DELETE FROM chunks statements deadlock on memory_links FK cascade (still occurring on 0.8.4)

2 participants