Skip to content

Fix permanent Lucene index corruption under concurrent indexing requests#35

Open
vany0114 wants to merge 2 commits into
feature23:mainfrom
DrivingThru:main
Open

Fix permanent Lucene index corruption under concurrent indexing requests#35
vany0114 wants to merge 2 commits into
feature23:mainfrom
DrivingThru:main

Conversation

@vany0114

@vany0114 vany0114 commented Jul 8, 2026

Copy link
Copy Markdown

Context

LuceneNetSearchIndexer.IndexDocuments opens a fresh IndexWriter per request. Lucene allows only a single IndexWriter per directory, so two concurrent indexing requests against the same index race:

  • the loser typically fails with LockObtainFailedException (HTTP 500), and
  • under sustained concurrency a torn commit permanently corrupts the segment files — every subsequent write to that index throws System.IO.FileNotFoundException: Could not find file '.../_N.si' until the index directory is deleted from disk.

We hit this in a CI environment where several services index into one shared index: 500 consecutive indexing requests failed after the corruption event, and the index never recovered on its own.

Repro

30 concurrent workers × 30 sequential mergeOrUpload requests against a freshly created index:

  • before: 11/900 requests succeed, 889 return 500; after the load stops, every sequential write still returns 500 (FileNotFoundException on segment files) — the index is permanently broken.
  • after: 900/900 succeed; the index stays healthy.

Change

A static ConcurrentDictionary<string, SemaphoreSlim> gates IndexDocuments per index name. Writes to the same index are serialized; requests against different indexes still run in parallel. Read paths are untouched.

Test plan

  • dotnet test — unit tests 134/134 pass; integration tests 24/25 (the single failure pre-exists on main and is unrelated).
  • Concurrency hammer described above against the docker image built from this branch.

🤖 Generated with Claude Code

vany0114 and others added 2 commits July 8, 2026 13:29
IndexDocuments creates a fresh IndexWriter per request. Lucene allows only
one IndexWriter per directory, so two concurrent indexing requests against
the same index race: the loser fails with LockObtainFailedException, and
under sustained concurrency a torn commit corrupts the segment files
permanently (FileNotFoundException: .../_N.si on every subsequent write
until the index directory is deleted from disk).

Reproduced with 30 concurrent writers x 30 sequential mergeOrUpload
requests against a fresh index: 889/900 requests returned 500 and the
index remained broken after concurrency stopped. With this change the
same load succeeds 900/900 and the index stays healthy.

A per-index SemaphoreSlim serializes IndexDocuments per index name;
requests against different indexes still run in parallel.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ch bodies, per-item outcomes

The emulator logged only [HTTP METHOD] path — no response status, no batch body, no per-item results. A rejected or dropped index merge was therefore invisible: the Azure SDK IndexDocuments does not throw on per-item failures by default, so neither the client nor the emulator left any trace (cost a full diagnosis session on the Curbit devbox: an order READIED merge reached the emulator but the doc never showed READIED, and nothing said why).

- request middleware logs the response status after the pipeline ([HTTP POST 207] ...) and any unhandled exception with type + message
- search.index logs the full batch body before deserialization (a malformed or wrong-fields payload is otherwise undiagnosable)
- search.index logs per-item outcomes keyed ([INDEX <idx>] 5/6 ok — key:200, key:404 FAILED(...)) — the only place a dropped write is guaranteed to surface

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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.

1 participant