- Each indexing thread fills its own in-memory segment (inside Lucene: a DWPT). Three threads indexing at once give three of them.
- A full flush (
DirectoryReader.open(writer),commit(),flush()) checks them all out, registers each in a set of flushing segments and queues them. A segment leaves that set only through one method,doAfterFlush. - After any
Error, IndexWriter closes itself. Part of that close waits until the set of flushing segments is empty.
- Three threads call
addDocumentat the same time on oneIndexWriterwith the default config, so the writer holds 3 in-memory segments of 1 document each. DirectoryReader.open(writer)flushes all 3. Writing segment 1 fails with anOutOfMemoryError(the repro simulates it inIndexOutput.close()), so the writer throws away segments 2 and 3.- You expect what the IndexWriter javadoc promises: the open throws the Error and the writer closes itself. On 7.3.1 an earlier, longer version of this test passes: the open throws and the writer is closed.
- Instead, a second Error while throwing away segment 2 ends the clean-up loop, which catches only
Exception. Segment 3 is never released and stays registered as flushing. - The self-close waits until no segment is flushing, so
DirectoryReader.open(writer)never returns: the repro times out after 20 s, with assertions on and off.
Applications that index from several threads and open NRT readers on the default configuration (commit and flush reach the same code), and keep the JVM running after an OutOfMemoryError. The thread hangs silently and the writer's self-close never finishes. It needs a failed full flush with 3 or more in-memory segments, then an Error at that step: rare.
Mechanism, terms and numbers433 words
When several threads index into one IndexWriter, each holds its own in-memory segment. A full flush (DirectoryReader.open(writer), commit(), flush()) queues them all. If flushing the first one fails, DocumentsWriterFlushControl.abortPendingFlushes aborts the rest, but it wraps each abort in catch (Exception _). An Error thrown while aborting one of them (in practice an OutOfMemoryError) ends the loop: the segments it has not reached stay registered in flushingWriters, and nothing ever removes them. IndexWriter then closes itself, as it documents for any Error, and that close waits in waitForFlush for those segments forever, so DirectoryReader.open(writer) never returns. Until LUCENE-8215 (2018, first release 7.4.0) the loop caught Throwable to "keep on aborting the flush queue". Every release since is affected. It is realistically OutOfMemoryError-only, so it is Low priority.
- Three segments queued.
DirectoryReader.open(writer)->IndexWriter.getReader->flushAllThreads()(IndexWriter.java:586).markForFullFlushchecks out every segment with documents;checkOutForFlushadds each toflushingWriters(DocumentsWriterFlushControl.java:427,:437-441) and all go intoflushQueue(:626). - The first flush fails.
maybeFlushtakes segment 1 from the queue (DocumentsWriter.java:494) and flushes it;IndexOutput.close()throws, anddoFlush'sfinallyreleases segment 1 withdoAfterFlush(DocumentsWriter.java:577). Segments 2 and 3 are still queued and registered. Any throwable in a segment's flush is an aborting exception, so the writer is already tragic here (DocumentsWriterPerThread.java:585-603); anIOExceptionwould do as well. - The abort loop meets an Error.
getReadercallsfinishFullFlush(false)(IndexWriter.java:669-671) ->abortFullFlushes(DocumentsWriter.java:738) ->abortPendingFlushes(DocumentsWriterFlushControl.java:694-723). Aborting segment 2 closes its open stored-fields writer throughIOUtils.closeWhileHandlingException(StoredFieldsConsumer.java:112-113), which rethrows Errors on purpose ("we ensure that we bubble up any errors",util/IOUtils.java:155-178). - Segment 3 is stranded.
catch (Exception _)(:700, and:712for blocked flushes) does not take the Error. The per-entryfinallyreleases segment 2 only, the Error leaves the loop, and the outerfinallyclears the queue (:718-722) while segment 3 is still influshingWriters. OnlydoAfterFlush(:298-301) removes it, and nothing will call it. - The self-close waits forever.
getReader'scatch (Throwable)marks the Error as tragic and runsmaybeCloseOnTragicEvent(IndexWriter.java:711-722) ->rollbackInternalNoCommit->docWriter.abort()(:2635) ->waitForFlush()(DocumentsWriter.java:226), an untimedwait()whileflushingWritersis not empty (DocumentsWriterFlushControl.java:361-369).
A RuntimeException thrown at the same point is caught and the open returns promptly, with no hang (the controls of the cross-model reviewers; 1.3 s in one), so the catch (Exception) boundary is the defect.
Exposure and likelihood210 words
- Who. Multi-threaded indexing (IndexWriter is documented as "completely thread safe",
IndexWriter.java:167) with NRT reopen, commit or flush, on the default configuration; the injected Error is a fault, not a setting. - Effect. A silent hang, not an exception: the thread that opened the reader blocks forever [exec]. By reading only:
commit()(IndexWriter.java:3875,:3879) andflush()(:4411,:4427-4434) reach the same loop. Executed by the fact check's probe: any laterrollback()orclose()of that writer never returns either, because the hung self-close has already setclosing(:6102,:1379), so they wait inshouldClose(true)->doWait()(:1387,:5867). - Data. The hang comes before the
write.lockrelease (:2635runs before:2684), so the last commit on disk is untouched,write.lockstays held in that JVM until the process exits, and buffered documents are lost, as in any tragic close [read]. - Scale. Three in-memory segments and one Error; no size threshold; it does not depend on assertions.
- How rare. Contrived: a full flush must fail, then an Error must hit the abort of a queued segment that is not the last one. Many servers exit the JVM on
OutOfMemoryError. No report of this hang was found since 7.4.0.
One JUnit test method, public classes only, default config (default codec pinned). Three threads each add one document at the same time (a latch holds them, so there are 3 in-memory segments); then every IndexOutput.close() throws a simulated OutOfMemoryError and the test calls DirectoryReader.open(writer). @Timeout(20 s) is the hang oracle. Deterministic; fails with assertions on and off.
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.lucene.index;
import com.carrotsearch.randomizedtesting.annotations.Timeout;
import java.io.IOException;
import java.io.StringReader;
import java.io.UncheckedIOException;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicBoolean;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.TextField;
import org.apache.lucene.store.ByteBuffersDirectory;
import org.apache.lucene.store.FilterDirectory;
import org.apache.lucene.store.FilterIndexOutput;
import org.apache.lucene.store.IOContext;
import org.apache.lucene.store.IndexOutput;
import org.apache.lucene.tests.util.LuceneTestCase;
import org.apache.lucene.tests.util.TestUtil;
public class TestErrorAbortingQueuedSegments extends LuceneTestCase {
// Expected: DirectoryReader.open throws the Error and the writer closes itself; actual: it hangs.
@Timeout(millis = 20_000)
public void testErrorWhileAbortingQueuedSegments() throws Exception {
AtomicBoolean fail = new AtomicBoolean();
FilterDirectory dir =
new FilterDirectory(new ByteBuffersDirectory()) {
@Override
public IndexOutput createOutput(String name, IOContext context) throws IOException {
return new FilterIndexOutput("x", name, super.createOutput(name, context)) {
@Override
public void close() throws IOException {
out.close();
if (fail.get()) throw new OutOfMemoryError("simulated");
}
};
}
};
IndexWriter writer =
new IndexWriter(dir, new IndexWriterConfig().setCodec(TestUtil.getDefaultCodec()));
CountDownLatch allInAddDocument = new CountDownLatch(3);
Thread[] threads = new Thread[3];
for (int i = 0; i < threads.length; i++) {
Document doc = new Document();
doc.add(
new TextField(
"f",
new StringReader("x") {
@Override
public int read(char[] buf, int off, int len) throws IOException {
allInAddDocument.countDown();
try {
allInAddDocument.await();
} catch (InterruptedException e) {
throw new IOException(e);
}
return super.read(buf, off, len);
}
}));
threads[i] =
new Thread(
() -> {
try {
writer.addDocument(doc);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
});
threads[i].start();
}
for (Thread t : threads) t.join();
fail.set(true);
expectThrows(OutOfMemoryError.class, () -> DirectoryReader.open(writer));
assertFalse(writer.isOpen());
}
}
cp $REPRO/TestErrorAbortingQueuedSegments.java $LUCENE/lucene/core/src/test/org/apache/lucene/index/
cd $LUCENE && ./gradlew :lucene:core:test --tests org.apache.lucene.index.TestErrorAbortingQueuedSegments -Ptests.seed=DEADBEEF
# also with -Ptests.asserts=false: the same hang
- Expected
DirectoryReader.open(writer)throws theOutOfMemoryErrorandwriter.isOpen()is false.- Observed
DirectoryReader.open(writer)never returns:Test timeout exceeded (>= 20000 msec), parked inDocumentsWriterFlushControl.waitForFlush:364.
More files, environment, full output419 words
TestErrorAbortingQueuedSegments (one method, extends LuceneTestCase, package org.apache.lucene.index of the lucene/core tests). The three threads only build the segments; the failing open runs on the test thread, and the outcome needs no timing. TestUtil.getDefaultCodec() pins the default codec against the test framework's random codec. No package-private member is used; FilterDirectory and FilterIndexOutput are public but marked @lucene.internal.
The same method is the regression test (it fits next to TestIndexWriterOnError). With a fix it passes in under 1.5 s. Older releases need more than package renames: FilterIndexOutput first shipped in 9.6.0 (GITHUB-11868, #11958), so before 9.6.0 wrap IndexOutput by hand, as the hunt's earlier, longer test for 7.x/8.x does; on 7.x/8.x the test framework package is org.apache.lucene.util, and on 7.x RAMDirectory replaces ByteBuffersDirectory.
The fault is harsher than a natural one-off OutOfMemoryError: once fail is set, every IndexOutput.close() throws. By reading, the hang needs only a failed flush of the first segment (any exception) and then one Error while a queued segment that is not the last is aborted. During the hang writer.isOpen() already returns false (IndexWriter.java:6117-6118), so it does not show that the close finished.
The timeout report also lists a leaked thread (the test thread still parked in waitForFlush) and a stdout-limit failure (the timeout's thread dump); both are consequences of the hang.
| lucene | main 5adff78bb168 (12.0.0-SNAPSHOT): hangs; 10.6.0 and 9.12.3: this test hangs too; with an earlier, longer version of the test, 7.4.0 to 10.6.0 hang (7 releases run) and 7.3.1 passes |
|---|---|
| jdk | OpenJDK 25.0.3 (build 25.0.3+9-LTS) |
| os | Linux aarch64, the only platform run (nothing platform-specific: plain Java exception handling and Object.wait()) |
cp $REPRO/TestErrorAbortingQueuedSegments.java $LUCENE/lucene/core/src/test/org/apache/lucene/index/
cd $LUCENE && ./gradlew :lucene:core:test --tests org.apache.lucene.index.TestErrorAbortingQueuedSegments -Ptests.seed=DEADBEEF
# gradle, scratch worktree at 5adff78bb168, -Ptests.seed=DEADBEEF (assertions on)
TestErrorAbortingQueuedSegments > testErrorWhileAbortingQueuedSegments FAILED
java.lang.Exception: Test timeout exceeded (>= 20000 msec).
at __randomizedtesting.SeedInfo.seed([DEADBEEF:C9B68B5A67B45519]:0)
... (the leak report, after the timeout):
com.carrotsearch.randomizedtesting.ThreadLeakError: 1 thread leaked from SUITE scope at org.apache.lucene.index.TestErrorAbortingQueuedSegments:
1) Thread[id=21, name=TEST-TestErrorAbortingQueuedSegments.testErrorWhileAbortingQueuedSegments-seed#[DEADBEEF]-worker, state=WAITING, group=TGRP-TestErrorAbortingQueuedSegments]
at org.apache.lucene.index.DocumentsWriterFlushControl.waitForFlush(DocumentsWriterFlushControl.java:364)
at org.apache.lucene.index.DocumentsWriter.abort(DocumentsWriter.java:226)
at org.apache.lucene.index.IndexWriter.rollbackInternalNoCommit(IndexWriter.java:2635)
at org.apache.lucene.index.IndexWriter.rollbackInternal(IndexWriter.java:2590)
at org.apache.lucene.index.IndexWriter.maybeCloseOnTragicEvent(IndexWriter.java:6103)
at org.apache.lucene.index.IndexWriter.getReader(IndexWriter.java:719)
at org.apache.lucene.index.DirectoryReader.open(DirectoryReader.java:144)
at org.apache.lucene.index.DirectoryReader.open(DirectoryReader.java:123)
at org.apache.lucene.index.TestErrorAbortingQueuedSegments.lambda$testErrorWhileAbortingQueuedSegments$1(TestErrorAbortingQueuedSegments.java:88)
at org.apache.lucene.tests.util.LuceneTestCaseParent._expectThrows(LuceneTestCaseParent.java:1587)
...
:lucene:core:test (FAILURE): 2 tests, 2 failures
# (the second failure is the suite-level report of the leaked thread and of the timeout's thread dump)
# same, -Ptests.asserts=false
TestErrorAbortingQueuedSegments > testErrorWhileAbortingQueuedSegments FAILED
java.lang.Exception: Test timeout exceeded (>= 20000 msec).
... leaked thread parked at org.apache.lucene.index.DocumentsWriterFlushControl.waitForFlush(DocumentsWriterFlushControl.java:364)
:lucene:core:test (FAILURE): 2 tests, 2 failures
# with fix A: assertions on, then off
:lucene:core:test (SUCCESS): 1 test
:lucene:core:test (SUCCESS): 1 test- The IndexWriter javadoc promises that after an Error the writer "will close itself"; here that close never finishes.
abortPendingFlusheswas created (LUCENE-4277, 2012) to stop exactly this kind of rollback hang, catchingThrowableto "keep on aborting the flush queue".- LUCENE-8215 (2018) narrowed it to
Exceptionwithout discussing this loop; the fix still lets the Error propagate.
The full argument243 words
- Contract. "if you hit an Error, or disaster strikes during a checkpoint then IndexWriter will close itself. ... Any subsequent calls will throw an AlreadyClosedException." (
IndexWriter.java:162-165).getReaderimplements this for every Error (:711-722). - Purpose of the loop. LUCENE-4277 (#5345,
b26dd733f77, 2012; CHANGES: "Fix IndexWriter deadlock during rollback if flushable DWPT instance are already checked out and queued up but not yet flushed") createdabortPendingFlusheswithcatch (Throwable ex) { // ignore - keep on aborting the flush queue }. LUCENE-4989 (#6053) and LUCENE-5002 (c0e7a50ab8e, 2013) moveddoAfterFlushinto a per-entryfinallyso a failing abort could not skip the release. - The narrowing. LUCENE-8215 (#9262,
2e35ef2b3da, 2018, "Fix several fragile exception handling places in o.a.l.index") changed both catches tocatch (Exception ex). Its discussion is about the new IOUtils helpers and never mentions this loop. The same commit addedIOUtils.applyToAll("even if an exception is thrown. The first exception ... is re-thrown"), the shape of the recommended fix. - Precedent. Upstream fixed Error-induced writer hangs as bugs: #12654 (fixed by #12751, 2023, an Error during the tragic close left
rollback()waiting forever) and LUCENE-7570, whose testTestTragicIndexWriterDeadlockthrows anOutOfMemoryErrorfrommergeSuccessand expectscommit()to fail fast andclose()to return. - Tests.
TestIndexWriterOnErrorinjects fake Errors from I/O but runs "just one thread" (:59of the test), so it never queues three segments in one full flush; no upstream test pins the hang.
The devil's-advocate check133 words
The strongest case for "not a bug": LUCENE-8215 narrowed the catch on purpose so that Errors are not swallowed, and after an OutOfMemoryError the JVM state is undefined, so Lucene owes nothing. Also, it needs two faults in one flush.
Why it fails: the narrowing meant that Errors should propagate, and the fix keeps that (it rethrows the first Error after every segment is released); nothing in #9262 says cleanup may stop halfway. IndexWriter itself documents a self-close after any Error and treats every Error as tragic; a hang breaks that promise and leaves the writer unclosable. The repro uses no InfoStream and no writer-internal hook: the Error comes from IndexOutput.close(), the kind of directory-I/O Error that Lucene's own TestIndexWriterOnError injects. Needing two faults lowers the likelihood (hence Low), not the validity.
Recommended Catch Error per entry, finish both loops, rethrow the first Error (fix A)
In abortPendingFlushes, also catch Error per segment, keep the first (later ones suppressed), finish both loops so every queued and blocked segment reaches doAfterFlush, then rethrow it (+11 lines with the import). The repro then passes with assertions on and off, and so do nine IndexWriter suites (77 tests, 4 @Nightly skipped).
+ Error firstError = null;
... for each queued DWPT, then each blocked DWPT:
} catch (Exception _) {
// that's fine we just abort everything here this is best effort
+ } catch (Error e) {
+ // keep aborting so every DWPT reaches doAfterFlush, rethrow below
+ firstError = IOUtils.useOrSuppress(firstError, e);
} finally {
doAfterFlush(dwpt);
... the outer finally clears flushQueue and blockedFlushes ...
+ if (firstError != null) {
+ throw firstError;
+ }All options and trade-offs2 options ยท 470 words
Option A was built and checked through gradle on 5adff78bb168; option B is the pre-7.4.0 code, which the hunt ran on 7.4.0. IOUtils.applyToAll is not a drop-in: it rethrows the first throwable of any type (util/IOUtils.java:566-577), so it would turn today's ignored abort exceptions into failures.
Catch Error per entry, finish both loops, rethrow the first Error (fix A) (validated by the checks) (recommended)
Every queued and blocked segment is aborted and released (unless doAfterFlush itself throws); the Error still propagates (after the cleanup), so LUCENE-8215's intent holds and the writer treats it as tragic. Exceptions stay ignored as today. The hunt's variant with explicit addSuppressed is the same idea and passed the repro too.
+ Error firstError = null;
... for each queued DWPT, then each blocked DWPT:
} catch (Exception _) {
// that's fine we just abort everything here this is best effort
+ } catch (Error e) {
+ // keep aborting so every DWPT reaches doAfterFlush, rethrow below
+ firstError = IOUtils.useOrSuppress(firstError, e);
} finally {
doAfterFlush(dwpt);
... the outer finally clears flushQueue and blockedFlushes ...
+ if (firstError != null) {
+ throw firstError;
+ }Trade-offs
If the full flush failed with an exception and the abort then hits an Error, finishFullFlush(false) now throws that Error in place of the flush's exception, as any throw from it does today.
What the checks validated
- Gradle in a scratch worktree at
5adff78bb168(OpenJDK 25.0.3, seedDEADBEEF): without the fix the repro times out at 20 s with assertions on and off; with fix A it passes with assertions on and off (0.38 s and 0.36 s test time). - With fix A: the repro,
TestIndexWriterOnError(5, 1@Nightlyskipped),TestTragicIndexWriterDeadlock(3),TestIndexWriterExceptions(40, 3@Nightlyskipped),TestIndexWriterExceptions2(1),TestIndexWriterWithThreads(14),TestIndexWriterOnDiskFull(4),TestNRTReaderWithThreads(1),TestFlushByRamOrCountsPolicy(5) andTestDocumentsWriterStallControl(3): 77 tests, 0 failures, 4 skipped.:lucene:core:checkGoogleJavaFormatpasses with the fix and the repro file in place. - The hunt's variant with explicit
addSuppressedpassed the same repro on the main jars too (assertions on 1.32 s, off 1.27 s, wall time).
Two lines. The hunt's 7.4.0 run with only the LUCENE-8215 hunk reverted passes (0.62 s wall time, writer closed; run with the earlier, longer version of the test).
Trade-offs
It swallows the abort's Error. The writer still closes itself, because the failed flush already made it tragic (DocumentsWriterPerThread.java:585-603), but if the flush failed with an IOException and the abort then hit an OutOfMemoryError, the OOM would be neither thrown nor recorded (the tragedy and the thrown exception stay the IOException; by reading), against LUCENE-8215's aim of not swallowing Errors.
What the checks validated
Executed on 7.4.0 only (the hunt's history step), not on main.
Downstream, a service that catches OutOfMemoryError and keeps serving, instead of exiting, can be left with a hung indexing thread and a writer that never finishes closing, until the process restarts.
Code reading of DocumentsWriterFlushControl: the exception handling of abortPendingFlushes was checked against the flushingWriters accounting that waitForFlush depends on. A JUnit test on public classes then witnessed the hang on main.
The method in full105 words
Method: non-formal (code reading, witnessed by an executable repro).
- The finder asked which throwables can leave
abortPendingFlushesand what each leaves behind influshingWriters, the setwaitForFlushwaits on.catch (Exception _)lets an Error out, the outerfinallyclears the queue, and onlydoAfterFlushremoves entries, so every segment after the one that threw stays registered. DWPT.abortcan throw an Error becauseIOUtils.closeWhileHandlingExceptionrethrows Errors by design, and the tragic self-close after an Error runswaitForFlush.- The finder's first test threw the Error from an InfoStream; an independent reproducer then wrote the Directory-based test this repro is reduced from, with no InfoStream.
No prior report in apache/lucene issues, PRs and commits, the JIRA archive, the mailing lists, downstream trackers or the web (searched 2026-11-03, re-checked). The nearest items are the history of this loop (#5345, #6053, #9262) and a fixed Error hang at another site (#12654).
Searches3 searches
Related, not reports: #5345 (LUCENE-4277, created the loop with catch (Throwable)), #6053 (LUCENE-4989, an abort exception skipped doAfterFlush; fixed by the per-entry finally), #9262 (LUCENE-8215, the introducing change), #12654/#12751 (an Error-induced tragic-close hang at another site, fixed 2023), #13277 (every Error tragic) and #10661 (LUCENE-9621, open: an OOM in flush trips an accounting assertion, no hang). The hunt's LFV-846 has the same waitForFlush symptom from a different cause and fix site (a throw in checkOutForFlush's stall logging); a fix for one does not fix the other.
| Where | When | How | Found |
|---|---|---|---|
| GitHub issues, PRs and commits (apache/lucene, the migrated JIRA archive) | 2026-11-03, re-checked | scripted prior-report searches (two runs: 25 queries with 20 hits, 2 rated high overlap, both this loop's own 2012 history, #5345 = LUCENE-4277; 67 queries with 170 hits, none high; 181 distinct URLs), 54 manual searches (abortPendingFlushes, flushingWriters, waitForFlush, 'keep on aborting', 'best effort', Error/OOM/tragic/hang combinations, test names), 11 JQL queries | no report; abortPendingFlushes issues: only #6053 |
| Open and recent PRs | 2026-11-03 | the changed files of all 409 open PRs; the 40 issues/PRs updated since 2026-11-01; upstream main's copy of the method | no PR touches the catch clauses; upstream main 1c2072f0284 still catches only Exception |
| Mailing lists, downstream, web | 2026-11-03 | dev@, java-user@, issues@ and commits@ (45 queries), Elasticsearch, OpenSearch, Solr and Lucene.NET trackers, 6 web queries | only commit mails of the history above and general OOM Q&A |
Introduced by LUCENE-8215 (2018), which narrowed the loop's catch (Throwable) to catch (Exception); first release 7.4.0. 7.3.1 passes, 7.4.0 hangs, and 7.4.0 with only that hunk reverted passes. All 59 releases since have the narrowed catch; the 7 run and main hang. Not fixed upstream.
Versions tested6 rows
Introduced by LUCENE-8215 (#9262, Simon Willnauer): 2e35ef2b3da on main (first release 8.0.0) and its branch_7x cherry-pick ddb0bd78f7d (first release 7.4.0), author date 2018-03-19, no CHANGES entry. The older parts of the mechanism (the outer finally, which LUCENE-4277 carried into the new abortPendingFlushes in 2012; the per-entry finally from LUCENE-5002, 2013) were harmless while the catch took Throwable.
Tag scan: all 59 release tags 7.4.0 to 10.6.0 catch Exception there, all 64 tags 4.0.0-beta to 7.3.1 catch Throwable (whitespace-normalized; 9.x and 10.x put an annotation between catch ( and Exception). Not fixed upstream: main 1c2072f0284 (2026-11-02) still catches only Exception; the newest commit on the file, #16867 (2026-10-30), leaves the method alone.
The release runs used the independent reproducer's earlier, longer version of this test (same class name and fault model, a hand-written IndexOutput wrapper, its own 20 s bound; adapted for 7.x/8.x). The test shown in this doc was run on main and, by the fact check, on the 10.6.0 and 9.12.3 jars: it hangs on all three.
| Version or commit | Verdict | Result |
|---|---|---|
main 5adff78bb168 | affected | gradle: hangs with assertions on and off; passes with fix A |
| 10.6.0, 10.5.2, 9.12.3 | affected | hang (-da; 10.6.0 and 9.12.3 also with this doc's test) |
| 8.0.0, 8.11.4 | 8.0.0: first main-line release | hang (-da, test adapted for the old framework) |
| 7.4.0, 7.7.3 | 7.4.0: first release | hang (-da) |
| 7.4.0 with the LUCENE-8215 hunk reverted | not affected | passes (0.62 s wall time), writer closed |
| 7.3.1 | last release before | passes (0.62 s wall time), writer closed |
The finder, an independent reproducer, two executed cross-model reviews (OpenAI GPT-5.6), a devil's-advocate verifier, an adjudication, an audit and a final repro check reproduced the hang and agree on the cause. This doc's gradle runs reproduced it on main and validated the fix.
Checks and verdicts293 words
The cross-model reviews ran on main ea57360912ed, whose abortPendingFlushes is identical to 5adff78bb168; every other check ran on 5adff78bb168 or a release.
| Check | Verdict | What it found |
|---|---|---|
| finder | reproduced | Own InfoStream-based test hangs on main (-ea, -da), 10.6.0 and 9.12.3; with the fix it passes and four suites pass 15 of 15. |
| cross-model review, refute lens (OpenAI GPT-5.6, executed) | real (high) | Own test hangs with -ea and -da; its RuntimeException control returns; the fix makes both pass. |
| independent reproducer | real | Own Directory-Error test (the basis of the repro) hangs on main (-ea, -da), 10.6.0, 10.5.2 and 9.12.3; with its fix the Error surfaces and the writer is closed. |
| cross-model review, intended lens (OpenAI GPT-5.6, executed) | real (high) | Own three-segment test hangs while the identical RuntimeException control returns in 1.3 s; with the fix TestIndexWriterOnError 4/4 and TestTragicIndexWriterDeadlock 3/3 pass. |
| devil's-advocate verifier | real (high) | Re-ran with another seed: hangs; own probe with no InfoStream hangs too; every 'intended' argument fails. |
| adjudication, audit and final repro check | confirmed, Low | Clean 5adff78bb168 re-runs fail with -ea and -da and pass with the fix; one deterministic test on public classes. |
| this doc | reproduced; fix A validated | Gradle in a scratch worktree: the repro hangs with assertions on and off, and with fix A it and nine IndexWriter suites pass; checkGoogleJavaFormat passes. |
| fact checks (Claude; OpenAI GPT-5.6) | real; wording fixed | Re-ran the repro through gradle (hangs with assertions on and off and with a second seed; passes with fix A), ran it on the 10.6.0 and 9.12.3 jars (hangs), and probed later rollback() and close() calls (never return). Their attribution and wording corrections are applied. |
The diagram as a still image, generated with AI and checked against the code. It is under 10 MB, the limit for a GitHub attachment.
- PNG 561 KB
Finding record, Lucene and upstream sources, repro files and diagram notes1,630 words
- Registry title
- An Error while IndexWriter aborts the queued in-memory segments of a failed full flush leaves the unvisited segments registered as flushing, so DirectoryReader.open(writer) and the writer's own self-close after the Error wait forever in waitForFlush (every release since 7.4.0)
- Area
- index / IndexWriter full-flush abort (DocumentsWriterFlushControl.abortPendingFlushes, doAfterFlush, waitForFlush)
- Priority
- Low, rank 796: On default settings, an Error (in practice an OutOfMemoryError) thrown while IndexWriter aborts a queued in-memory segment after a failed full flush makes DirectoryReader.open(writer) and the writer's own self-close wait forever (later rollback and close calls too, executed; by reading also commit and flush), in every release since 7.4.0. It needs a failed flush with three or more queued segments followed by an Error at that exact step, and many servers exit the JVM on an OutOfMemoryError anyway, so it is contrived and Low.
- Impact class
- hang
- Impact note
- Hits applications on the default configuration that index from several threads and open NRT readers when a full flush fails and an Error, realistically an OutOfMemoryError, is then thrown while the writer aborts a still-queued in-memory segment that is not the last one: DirectoryReader.open(writer) and the tragic self-close it starts never return [exec, -ea and -da], and by reading the same happens through commit() and flush(); any later rollback() or close() of that writer never returns either, because it waits in shouldClose -> doWait for the hung self-close [exec: the fact check's probe, -da]; no scale is needed (three in-memory segments), and every release from 7.4.0 through 10.6.0 and main is affected.
- Summary
- Several threads index into one IndexWriter, so it holds three or more in-memory segments, and you open an NRT reader; the full flush fails, and while the writer aborts the segments still queued behind the failed one an Error (realistically an OutOfMemoryError) is thrown. You expect what the IndexWriter javadoc promises: after an Error the writer closes itself, DirectoryReader.open(writer) throws, and later calls throw AlreadyClosedException. Instead abortPendingFlushes catches only Exception, so the Error leaves its loop after releasing just the current segment, the segments it has not visited yet stay registered as flushing with nothing left to release them, and the writer's self-close waits for them forever, so DirectoryReader.open(writer) never returns.
- Introduced
- 2e35ef2b3daa1c1fdccbf78326d589e873489311 on the main line (first release 8.0.0) and its branch_7x cherry-pick ddb0bd78f7dbf207f8bf78fdcbb68914d98cdc3a (first release 7.4.0), the same change: LUCENE-8215 'Fix several fragile exception handling places in o.a.l.index' by Simon Willnauer (migrated issue #9262; reviewed on a pull request in the author's own fork, which now returns 404; no CHANGES entry). It changed both per-entry 'catch (Throwable ex) { // ignore - keep on aborting the flush queue }' clauses of DocumentsWriterFlushControl.abortPendingFlushes to 'catch (Exception ex)' [read: git log -L on abortPendingFlushes in the history clone; exec: 7.3.1 passes, 7.4.0 hangs, and 7.4.0 with only that hunk reverted in a shadow class passes]. The rest of the mechanism is older and was harmless while the catch took Throwable: the outer finally that clears the queues (LUCENE-4277, b26dd733f77, 2012) and the per-entry finally doAfterFlush (LUCENE-5002, c0e7a50ab8e, 2013). LUCENE-8205 and LUCENE-8212, both authored a few days before LUCENE-8215, let an Error out of DWPT.abort through IOUtils.closeWhileHandlingException; an Error from the InfoStream's 'now abort' line escapes DWPT.abort even without them. Individual 2018 commits were not built. (2018-03-19 (author date of 2e35ef2b3da and ddb0bd78f7d; committed 2018-03-20 on main and branch_7x); first release 7.4.0)
- First affected version
- 7.4.0 [exec: the independent reproduction's earlier, longer version of the one-test repro (same class name and fault model), adapted to the 7.x test framework, hangs with -da]; 7.3.1 is not affected [exec: passes, the writer ends closed]; on the main line the first release is 8.0.0 [exec: hangs]. Releases before 4.0.0-beta have no abortPendingFlushes, and 4.0.0-beta to 7.3.1 catch Throwable there [read: tag scan]
- Affected versions
- Every release from 7.4.0 (2018) through 10.6.0 (released 2026-10-29, 4ae946e5a9b) and main 5adff78bb168 (12.0.0-SNAPSHOT); never fixed. [exec, JDK 25, Linux aarch64] the one-test repro TestErrorAbortingQueuedSegments hangs on main with -ea and -da (the final repro check and gradle) and on the 10.6.0 and 9.12.3 jars (-da, the fact check); the independent reproduction's earlier, longer version of it (same class name and fault model) hangs on main (-ea and -da: the independent reproduction, the re-run of 2026-11-03 and the audit; -da: the adjudication), on 10.6.0, 10.5.2 and 9.12.3 (-da, the independent reproduction; 10.6.0 and 9.12.3 again in the history step) and, adapted for the 7.x/8.x test framework, on 7.4.0, 7.7.3, 8.0.0 and 8.11.4 (-da); 7.3.1 passes. The finder's InfoStream-based test hangs on main (-ea, -da), 10.6.0 and 9.12.3 (-da). [read] all 59 release tags from 7.4.0 to 10.6.0 in the release-tag clone have catch (Exception) in this loop and all 64 tags from 4.0.0-beta to 7.3.1 have catch (Throwable); 8.9.0 to 8.11.3 are not in the clone and fall between 8.8.1 (read) and 8.11.4 (exec). Not new in 10.6 (10.5.2 and every executed release from 7.4.0 hang). Not fixed upstream: main 1c2072f0284 (2026-11-02), branch_11x 6c5889ee89f and branch_10_6 5deb5791f6c still catch only Exception there; the last commit on DocumentsWriterFlushControl.java is #16867 (2813efa40a3, author date 2026-10-30), which 5adff78 contains and which leaves these catches alone.
- Status
- documented (brief, checks and a per-finding doc exist)
- Verification
- Confirmed by AI-run checks: an independent repro, a cross-model review that agreed, an intended-behaviour check, a prior-report search and a skeptical audit; not yet triaged by a person.
- Found by
- non-formal: code reading
- Upstream and prior reports
- none found (2026-11-03, on top of the finder's, the independent reproduction's, the intended-behaviour check's and the adjudication's searches; rechecked the same day): two scripted prior-report searches (the finder's 25 queries with 20 distinct hits, and the follow-up's 67 queries over apache/lucene issues, PRs and commits, Elasticsearch, OpenSearch and Solr, the dev@, java-user@, issues@ and commits@ lists, the JIRA archive and the web, 170 distinct hits, none high), 54 manual GitHub searches, the changed files of all 409 open apache/lucene PRs, the abortPendingFlushes code on main, branch_11x and branch_10_6, and the commit history of DocumentsWriterFlushControl.java on main (author dates); the same-day recheck found no issue or PR about abortPendingFlushes beyond #6053, no issue or PR on waitForFlush or DocumentsWriterFlushControl updated since 2026-11-01, and none of the 40 PRs updated since 2026-11-01 is about flush aborts. Related history, not reports: #5345 (LUCENE-4277, 2012: created this loop with catch (Throwable) 'keep on aborting the flush queue' to fix a rollback hang in waitForFlush), #6053 (LUCENE-4989, 2013: an exception from dwpt.abort() in this loop skipped doAfterFlush; fixed by the per-entry finally), #9262 (LUCENE-8215, 2018: the introducing change; its discussion is about the new IOUtils helper, not Errors in this loop) and #12654 (2023, fixed: an Error during the tragic close hung IndexWriter at another site).
- Lucene commit tested
- main
5adff78bb168(2026-11-02) through gradle in a scratch worktree, with assertions on and off, without and with the recommended fix (the repro, nine IndexWriter suites,checkGoogleJavaFormat), and standalone on the main jars; the fact check also ran it on the 10.6.0 and 9.12.3 jars (hangs). The hunt ran an earlier, longer version of the test (same fault model) on 7.3.1 (pass), 7.4.0, 7.4.0 with the LUCENE-8215 hunk reverted (pass), 7.7.3, 8.0.0, 8.11.4, 9.12.3, 10.5.2 and 10.6.0. Only Linux aarch64 with OpenJDK 25.0.3 was run.
- Finding record
lucene/core/src/java/org/apache/lucene/index/DocumentsWriterFlushControl.java:694-723 (at 5adff78bb168) (abortPendingFlushes: per queued DWPT 'try { subtractFlushedNumDocs; dwpt.abort(); } catch (Exception _) { // that's fine we just abort everything here this is best effort } finally { doAfterFlush(dwpt); }', the same for blockedFlushes; the outer finally at :718-722 clears flushQueue and blockedFlushes), :685-692 (abortFullFlushes -> abortPendingFlushes), :298-301 (doAfterFlush, the only removal from flushingWriters), :361-369 (waitForFlush: untimed wait while flushingWriters is non-empty), :427 and :437-441 (a DWPT enters flushingWriters when it is checked out for flushing, before it is queued)lucene/core/src/java/org/apache/lucene/index/DocumentsWriterPerThread.java:98-115 (at 5adff78bb168) (abort: 'now abort' / 'done abort' InfoStream lines around indexingChain.abort(); an Error propagates); lucene/core/src/java/org/apache/lucene/index/StoredFieldsConsumer.java:112-113 (at 5adff78bb168) (abort -> IOUtils.closeWhileHandlingException(writer)); lucene/core/src/java/org/apache/lucene/util/IOUtils.java:155-178 (at 5adff78bb168) (closeWhileHandlingException rethrows Errors: 'we ensure that we bubble up any errors')lucene/core/src/java/org/apache/lucene/index/DocumentsWriter.java:212-226 (at 5adff78bb168) (abort: abortDocumentsWriterPerThread for the pool, then flushControl.abortPendingFlushes and flushControl.waitForFlush at :226), :727-739 (finishFullFlush(false) -> flushControl.abortFullFlushes)lucene/core/src/java/org/apache/lucene/index/IndexWriter.java:162-165 (at 5adff78bb168) (class javadoc: 'if you hit an Error ... IndexWriter will close itself ... Any subsequent calls will throw an AlreadyClosedException'), :711-722 (getReader: catch Throwable, onTragicEvent for an Error, maybeCloseOnTragicEvent, rethrow), :2630-2636 (rollbackInternalNoCommit: docWriter.abort() at :2635 before deleter.refresh at :2671 and the write.lock release at :2684), :3773-3884 (prepareCommitInternal: finishFullFlush(flushed) at :3875, catch (Error tragedy) at :3879, maybeCloseOnTragicEvent in the finally) and :4383-4411 (doFlush: finishFullFlush(flushSuccess) at :4411); commit and flush reach the same path, by reading
- The finding in 30 seconds
lucene/core/src/java/org/apache/lucene/index/IndexWriter.java:586, (at 5adff78bb168) :669-671, :711-722, :2630-2636lucene/core/src/java/org/apache/lucene/index/DocumentsWriter.java:212-226, (at 5adff78bb168) :494, :577, :727-739lucene/core/src/java/org/apache/lucene/index/DocumentsWriterFlushControl.java:298-301, (at 5adff78bb168) :361-369, :427, :437-441, :569-631, :685-723lucene/core/src/java/org/apache/lucene/index/StoredFieldsConsumer.java:112-113; (at 5adff78bb168) IndexingChain.java:530-538; DocumentsWriterPerThread.java:98-115, :585-603; ../util/IOUtils.java:155-178
- Who is affected
lucene/core/src/java/org/apache/lucene/index/IndexWriter.java:162-167, (at 5adff78bb168) :1312-1319, :1374-1388, :2575-2583, :2635, :2684, :5860-5870, :6097-6104, :6117-6118, :3773-3884, :4383-4436
- Why it looks unintended
lucene/core/src/java/org/apache/lucene/index/IndexWriter.java:162-165, (at 5adff78bb168) :711-722lucene/CHANGES.txt:8741-8743 (at 5adff78bb168) (LUCENE-7570); lucene/core/src/test/org/apache/lucene/index/TestTragicIndexWriterDeadlock.java:101-175; TestIndexWriterOnError.java:59, :234-261lucene/core/src/java/org/apache/lucene/index/../util/IOUtils.java:566-577 (at 5adff78bb168) (applyToAll)
- Suggested fix
lucene/core/src/java/org/apache/lucene/index/../util/IOUtils.java:553-577 (at 5adff78bb168) (useOrSuppress, applyToAll)lucene/core/src/java/org/apache/lucene/index/DocumentsWriterPerThread.java:585-603 (at 5adff78bb168) (a failed flush fires onTragicEvent)
- Diagrams
lucene/core/src/java/org/apache/lucene/index/DocumentsWriterFlushControl.java:298-301, (at 5adff78bb168) :361-369, :694-723
- TestErrorAbortingQueuedSegments.java (lucene/core, package org.apache.lucene.index, 1 test):
repro/LFV-1174/TestErrorAbortingQueuedSegments.java(91 lines)
The diagram illustrates the repro's scenario (main 5adff78bb168, DocumentsWriterFlushControl.java:298-301, :361-369, :694-723). The repro rows carry the line numbers of the test file shown in this doc. The flush states drawn are the diagram probe's run with assertions off; with assertions on the segments came in another order, with the same outcome.
assets/LFV-1174/bug.png(561 KB)
