If the merge scheduler's InfoStream throws on the log line right after IndexWriter hands out a merge, that merge stays marked as running with no thread to run it, so the writer hangs in its own tragic rollback after an Error, or in close() after an exception (8.6.0 to main)
Finding LFV-844Area: index / IndexWriter merge hand-off to the merge scheduler (IndexWriterMergeSource.getNextMerge, runningMerges)Priority: lowImpact: 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
You turn on IndexWriter's InfoStream for merge-scheduler messages, and the logging call throws once (an OutOfMemoryError, or an exception from the logging backend). It throws on the message the writer prints right after it hands a merge to the merge scheduler. You expect what the IndexWriter javadoc promises: after an Error the writer closes itself, and after an exception close() still finishes, because no merge is actually running. Instead, the merge has already been moved from 'pending' to 'running', and no thread ever runs or finishes it, so after an Error addDocument hangs forever inside the writer's own rollback (abortMerges), and after an exception close() hangs forever in waitForMerges.
The test shows that when the merge scheduler's InfoStream throws an OutOfMemoryError on the " checked out merge" line, the merge is already in runningMerges with no thread to run it, so the writer's own tragic rollback (abortMerges) waits for it and addDocument never returns.
Expected vs actual
Expected the 4th addDocument to throw the Error and the writer to close itself; actual expected addDocument to throw the Error, still blocked after 10 s (the framework's thread-leak report then shows the thread in IndexWriter.abortMerges <- maybeCloseOnTragicEvent), with -ea and -da.
Files
Run it
- Tested on Lucene main e1faeba08bde (2026-11-02): still reproduces.
- Tested on Lucene main 645d4fe14a20 (2026-10-28): reproduced.
- Recorded outcome of run 1 on Lucene main 645d4fe14a20: 1 test(s) run, 2 failed.
- 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
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.TestErrorLoggingCheckedOutMerge
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).
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.