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

Finding page for LFV-844

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

# 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.