Repository navigation
fix(retain): pre-extraction freshness recheck + serialize concurrent same-doc writers - #1930
Merged
Merged
Conversation
nicoloboschi
force-pushed
the
repro-delta-concurrency-1916
branch
2 times, most recently
from
June 2, 2026 15:37
943d1c2 to
c18c979
Compare
…same-doc writers Two fixes for concurrent retains targeting the same document: 1. Delta path now re-reads the document hash BEFORE the (expensive) LLM extraction. If a concurrent retain already committed identical content, we skip extraction and update metadata only; if it still differs we fall back to streaming. This avoids burning LLM tokens re-extracting work a concurrent request already did (staggered 10-way race: 10 -> 1 extraction call). 2. Streaming write-txn ownership gate is now a single atomic INSERT ... ON CONFLICT DO UPDATE (which locks the row) instead of INSERT ON CONFLICT DO NOTHING + a separate SELECT FOR UPDATE. DO NOTHING does not lock the existing row, which let concurrent same-document writers interleave the speculative-insert ShareLock with the later FOR UPDATE and cascade-DELETE in inconsistent orders, producing Postgres deadlocks. Adds tests/test_retain_same_document_concurrency.py covering: identical concurrent retains skip extraction, partial-overlap race completes cleanly, staggered race avoids redundant extraction, and fully-different concurrent retains no longer deadlock.
nicoloboschi
force-pushed
the
repro-delta-concurrency-1916
branch
from
June 2, 2026 15:55
c18c979 to
fbc6134
Compare
This was referenced Jun 3, 2026
cdbartholomew
added a commit
that referenced
this pull request
Oct 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.
Problem
Concurrent retains targeting the same document have two issues, both reproduced empirically (see
tests/test_retain_same_document_concurrency.py):Redundant LLM extraction in the delta hash-mismatch race. When a concurrent retain commits a new version of a document between our chunk-load and our write, the delta path falls back to a full re-process — but the expensive LLM extraction (
_extract_and_embed) has already run by then, because it happens before the post-load hash check. Acting only after the spend can't recover the tokens.Postgres deadlock in the streaming fallback. Concurrent same-document retains with non-overlapping content deadlock (
DeadlockDetectedError, 3-way lock cycle inhandle_document_tracking). The ownership gate usedINSERT ... ON CONFLICT DO NOTHING(which does not lock the existing row) followed by a separateSELECT ... FOR UPDATE, letting writers interleave the speculative-insert ShareLock, the FOR UPDATE, and the cascade-DELETE in inconsistent orders.Fix
Pre-extraction freshness recheck (
_try_delta_retain): re-read the document hash immediately before extraction. If a concurrent retain already committed identical content, skip extraction and update metadata only; if it still differs, fall back to streaming. This acts before the LLM spend.Atomic ownership gate (
_streaming_retain_batch): replaceINSERT ON CONFLICT DO NOTHING+SELECT FOR UPDATEwith a singleINSERT ... ON CONFLICT DO UPDATE SET content_hash = documents.content_hash RETURNING content_hash.DO UPDATEtakes the row lock atomically, so all writers serialize on the document row in one consistent step.Extracted the chunk-diff classification into
_classify_chunk_diffreturning a typed_ChunkDiff(no tuple return), reused by both the main diff and the recheck.Verification
tests/test_retain_same_document_concurrency.py(counts realfact_extraction.extract_facts_from_contentscalls; stub embeddings because the local torch model segfaults under in-process async concurrency):Note: a tight simultaneous burst still extracts N times — the recheck can't see commits that haven't happened yet. Fully eliminating that would require per-document serialization (advisory lock), intentionally out of scope here.
All existing delta/retain suites pass; lint +
tyclean.Risk
Low. The recheck adds one indexed PK lookup to delta retains that have changes and otherwise preserves behavior (the post-extraction hash gate remains the correctness backstop). The ownership-gate change is a strict tightening of locking on a path that already used
ON CONFLICT.