IndexWriter.deleteAll can deadlock with a concurrent flush publish and a concurrent tryDeleteDocument or addIndexes, because DocumentsWriter.lockAndAbortAll waits for the flush-ticket purge lock while holding the DocumentsWriter monitor
Finding LFV-1137Area: index / IndexWriter deleteAll (IndexWriter-DocumentsWriter lock order)Priority: mediumImpact: hangNovelty: new
AI-assisted finding, confirmed by automated checks, not yet triaged by a human Lucene committer. This site files nothing upstream.
What it shows
Three threads share one IndexWriter: one adds documents and its addDocument() flushes and publishes the new segment, one calls tryDeleteDocument() on an NRT reader (or addIndexes()), and one calls deleteAll(). You expect all three calls to return, because IndexWriter is documented as completely thread safe for concurrent calls of any of its methods. Instead they can deadlock for good: deleteAll() holds the DocumentsWriter monitor (in lockAndAbortAll) and waits for the flush-ticket purge lock, the publishing thread holds that purge lock and waits for the IndexWriter monitor (publishFlushedSegment), and tryDeleteDocument() (or addIndexes()) holds the IndexWriter monitor and waits for the DocumentsWriter monitor to get a sequence number (getNextSequenceNumber).
Expected vs actual
- Expected: All three calls return:
tryDeleteDocumentgets its sequence number, the flushed segment is published anddeleteAllfinishes (as with the fix). - Actual:
AssertionErrorlisting 3 JVM-detected deadlocked threads, stuck ingetNextSequenceNumber,forcePurgeandpublishFlushedSegment, with assertions on and off.
Files
Run it
- Tested on Lucene main 5adff78bb168 (2026-11-02): reproduced.
- How to read the outcome: a failing test is the finding reproducing (the failure message states expected against actual).
- Seeds: a rerun with the same seed replays the same random choices. Assertions off (-da -dsa -Dtests.asserts=false) is the production-like setting.
# Lucene checkout at the tested commit, built with: ./gradlew --no-daemon assemble (JDK 25)
LUCENE=/path/to/lucene; V=12.0.0-SNAPSHOT
CP=$LUCENE/lucene/core/build/libs/lucene-core-$V.jar
CP=$CP:$LUCENE/lucene/codecs/build/libs/lucene-codecs-$V.jar
CP=$CP:$LUCENE/lucene/test-framework/build/libs/lucene-test-framework-$V.jar
CP=$CP:randomizedtesting-runner-2.9.2.jar:junit-4.13.2.jar:hamcrest-3.0.jar # test-framework dependencies (Maven Central)
CP=$CP:junit-platform-console-standalone-6.1.3.jar # JUnit Platform console launcher (Maven Central)
javac -d classes -cp "$CP" $(find src -name '*.java')
# run 1: assertions on, seed DEADBEEF, default flags
java -ea -esa --add-modules jdk.incubator.vector --enable-native-access=ALL-UNNAMED -Dtests.seed=DEADBEEF \
-cp "classes:$CP" org.junit.platform.console.ConsoleLauncher \
execute --select-class org.apache.lucene.index.TestDeleteAllDeadlock
Gradle alternative: copy the test into the matching module's src/test/ tree of the checkout and run ./gradlew -p lucene/<module> test --tests <class> -Ptests.seed=<seed> (add -Ptests.asserts=false for a run without assertions).
Files shown on the finding page
Feedback
Open an issue in lucene-ai-bug-hunt. Please do not open issues or pull requests in apache/lucene about this finding unless a Lucene committer agrees it should be filed.
Snapshot of 2026-11-04. AI-assisted; not yet triaged by a human Lucene committer. Feedback: open an issue in lucene-ai-bug-hunt.