Since 9.4.0, addIndexes(CodecReader...) is carried out as a merge that the merge scheduler runs while the caller waits; NoMergeScheduler runs nothing.
- An
IndexWriterhas a merge policy, which decides which merges to do, and a merge scheduler, which runs them (by defaultConcurrentMergeScheduler, on background threads). NoMergePolicyselects no merges;NoMergeSchedulerruns none. TheNoMergeSchedulerjavadoc recommends using both to disable merging.addIndexes(CodecReader...)copies the segments of readers you pass into the writer. Since 9.4.0 that copy is itself a merge, chosen byMergePolicy.findMerges(CodecReader...)(one merge of all readers by default) and run by the merge scheduler.
addIndexes(CodecReader...) hands its one merge to NoMergeScheduler, which never runs it, and then waits on it with no time limit; up to 9.3.0 the call merged the reader itself and returned. Select the still to open it at full size. Generated with AI, checked against the code.- Index 1 document, open its segment as a
CodecReader, and open a second, empty writer withNoMergeSchedulerandNoMergePolicy, the pair theNoMergeSchedulerjavadoc recommends to disable merging. CalladdIndexes(reader). - You expect the document to be added, as 9.3.0 does (the whole test takes 1.3 s there), or at least that the call returns or throws.
- Since 9.4.0 the call turns the reader into a merge, hands it to the merge scheduler and waits on it with no time limit.
NoMergeScheduler.mergeis empty, so nobody ever runs that merge. - The call is still blocked after 10 s, parked in
MergeSpecification.await(3 ms of CPU after 4 s). Only another thread can release it, with an exception: an interrupt, or anything that aborts the writer's merges, such asrollback(),close()ordeleteAll(). A single-threaded caller never gets control back. The document is not added and no merge output is written.
Applications that set the Expert option NoMergeScheduler (alone, with the default policy or with NoMergePolicy) and call addIndexes(CodecReader...), for example to copy, filter or split segments into a writer with merges disabled. Every such call blocks, at any size (with NoMergePolicy or the default policy). addIndexes(Directory...) and schedulers that run merges are unaffected. Rare: no report found in four years.
Mechanism, terms and numbers464 words
Up to 9.3.0, addIndexes(CodecReader...) merged the readers itself, in the calling thread, and its javadoc said it did not use the MergeScheduler. Since 9.4.0 (LUCENE-10216) it asks the merge policy for a merge of the readers, hands that merge to the merge scheduler and waits for it with an unbounded CompletableFuture.get(). NoMergeScheduler.merge does nothing, so the merge never starts and the call blocks until another thread interrupts it (ThreadInterruptedException) or aborts the writer's merges, for example with rollback(), close() or deleteAll() (MergeAbortedException). NoMergePolicy keeps that merge on purpose so that addIndexes keeps working with merges disabled, so the pair the NoMergeScheduler javadoc recommends hangs too. Every release from 9.4.0 to 10.6.0 and main is affected; nothing is corrupted and the readers are simply not added. The claim is narrow: the call should return or throw, not block.
- The hand-off and the wait (
IndexWriter.java:3306-3313, all from LUCENE-10216):
MergePolicy.MergeSpecification spec = mergePolicy.findMerges(readers);
boolean mergeSuccess = false;
if (spec != null && spec.merges.size() > 0) {
try {
spec.merges.forEach(addIndexesMergeSource::registerMerge);
mergeScheduler.merge(addIndexesMergeSource, MergeTrigger.ADD_INDEXES);
spec.await();registerMergeonly estimates the merge's size and queues it (IndexWriter.java:3408-3417); only a scheduler that callsgetNextMerge()andmerge(...)on that source runs it.- No time limit:
MergeSpecification.await()is "Waits, until interrupted, for all merges to complete", a plainCompletableFuture.get()(MergePolicy.java:533-544). A boundedawait(long, TimeUnit)exists (:546-557) but is not used here. - The scheduler does nothing:
public void merge(MergeSource mergeSource, MergeTrigger trigger) {}(NoMergeScheduler.java:45), empty since the class was added in 2010. TheMergeSchedulercontract is "Run the merges provided byMergeSource#getNextMerge()" (MergeScheduler.java:43-49). - The policy still asks for the merge:
NoMergePolicy.findMerges(CodecReader...)returns the default single merge, "to enable addIndexes for consumers like IndexRearranger" (NoMergePolicy.java:42-50). The defaultTieredMergePolicydoes not override that method and returns the same single merge. - Only an abort or an interrupt releases it: everything that calls
abortMerges()does, for examplerollback()andclose()(throughrollbackInternal(),IndexWriter.java:2621) anddeleteAll()(:2771; its javadoc warns that concurrentaddIndexes(CodecReader[])callers may receiveMergeAbortedException,:2743-2745).abortMerges()aborts the queued addIndexes merges (:2821-2837,:3443-3454) andaddIndexesthen throwsMergeAbortedException("merge was aborted.")(:3376-3378). An interrupt givesThreadInterruptedException(MergePolicy.java:539-540) but leaves the unstarted merge queued until something aborts merges. The parked thread holds no writer lock: the call'ssynchronizedblock ends before the hand-off (IndexWriter.java:3298-3304). - Not a race: it waits on a merge that nothing executes. Deterministic, seed-independent, with assertions on and off.
addIndexes(Directory...) with the same configuration returns and adds the documents (3 -> 8 docs in a verifier's control). The merge-running schedulers that were tested work: SerialMergeScheduler and the default ConcurrentMergeScheduler, each with NoMergePolicy.
Exposure and likelihood222 words
- Who. Code that configures
NoMergeScheduler(IndexWriterConfig.setMergeScheduleris documented as "Expert",IndexWriterConfig.java:271-279) and adds readers withaddIndexes(CodecReader...). It hangs withNoMergePolicyand with the defaultTieredMergePolicy. - Effect. The first call blocks, deterministically, with any number of documents (the repro adds 1), whenever the merge policy returns a non-empty reader-merge spec:
NoMergePolicyand the defaultTieredMergePolicyboth do; a custom policy returningnullor an empty spec makes the call return early (IndexWriter.java:3325-3338). The thread parks without CPU use; a single-threaded caller never gets control back. The readers are not added and no merge output is written; nothing is corrupted. The flush at the start of the call (IndexWriter.java:3286) still writes the target's own buffered changes, if it has any. - In-tree reach. The benchmark module accepts
merge.scheduler=NoMergeScheduler(CreateIndexTask.java:110-113) andAddIndexes(false)callsaddIndexes(CodecReader...)(AddIndexesTask.java:72,conf/addIndexes.alg:41): a verifier ran that combination, which hangs on main and returns on 9.3.0.PKIndexSplittertakes caller-supplied configs and callsaddIndexes(CodecReader...), so a caller passingNoMergeSchedulerwould hit it (code reading).IndexRearrangerbuilds its own configs with the default scheduler and is not affected. - Not hit.
addIndexes(Directory...); the merge-running schedulers that were tested,SerialMergeSchedulerandConcurrentMergeScheduler, each withNoMergePolicy. - Likelihood: rare. The pairing is unusual; the recorded searches found no report since 9.4.0 (2022).
One JUnit class, one method, public API only: index 1 document, open a second writer with NoMergeScheduler and NoMergePolicy, call addIndexes with that segment reader on a watchdog thread, and assert that it finished within 10 s. rollback() then releases the blocked call. It 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 org.apache.lucene.document.Document;
import org.apache.lucene.store.ByteBuffersDirectory;
import org.apache.lucene.store.Directory;
import org.apache.lucene.tests.util.LuceneTestCase;
public class TestAddIndexesNoMergeScheduler extends LuceneTestCase {
public void testAddIndexesCodecReaderReturns() throws Exception {
try (Directory src = new ByteBuffersDirectory();
Directory dst = new ByteBuffersDirectory()) {
try (IndexWriter sw = new IndexWriter(src, new IndexWriterConfig())) {
sw.addDocument(new Document());
}
try (DirectoryReader r = DirectoryReader.open(src)) {
CodecReader leaf = (CodecReader) r.leaves().get(0).reader();
IndexWriter w =
new IndexWriter(
dst,
new IndexWriterConfig()
.setMergeScheduler(NoMergeScheduler.INSTANCE)
.setMergePolicy(NoMergePolicy.INSTANCE));
Thread t =
new Thread(
() -> {
try {
w.addIndexes(leaf);
} catch (Exception returnedByThrowing) {
}
});
t.start();
t.join(10_000);
boolean blocked = t.isAlive();
w.rollback(); // releases a blocked call with MergeAbortedException
t.join();
assertFalse("addIndexes must return or throw, but was still blocked after 10 s", blocked);
}
}
}
}
cd $LUCENE && ./gradlew :lucene:core:test --tests org.apache.lucene.index.TestAddIndexesNoMergeScheduler -Ptests.seed=DEADBEEF
- Expected
addIndexesreturns (adding the document) or throws within 10 s; on 9.3.0 it returns at once.- Observed
addIndexes must return or throw, but was still blocked after 10 s; the thread is parked inMergeSpecification.awaitcalled fromIndexWriter.java:3313.
More files, environment, full output323 words
The class sits in org.apache.lucene.index so it can live next to TestAddIndexes upstream; it uses no package-private access. Smallest input: one empty document, one segment, ByteBuffersDirectory; the source writer uses a default IndexWriterConfig, the target writer sets only NoMergeScheduler and NoMergePolicy. The extra thread is only a watchdog, because a hang cannot be bounded on the calling thread. After waiting up to 10 s the test records whether the call is still running, calls rollback(), which releases a blocked call (so no thread leaks), waits for the thread, and fails if the call had been blocked. Any exception from the call counts as returning, so a fail-fast fix passes.
Regression test: this same class (or its one method added to TestAddIndexes); it passes with the recommended fix, well under a second. It accepts any exception, so with the fail-fast fix an expectThrows(IllegalStateException.class, ...) on the calling thread pins the fix more tightly.
| lucene | main 645d4fe14a20 (11.0.0-SNAPSHOT): fails; releases 10.6.0 and 9.4.0: fail; 9.3.0: passes; with the fix: passes |
|---|---|
| jdk | OpenJDK 25.0.3 (build 25.0.3+9-LTS) |
| os | Linux, aarch64 (the only platform run; a wait on a future that nothing completes) |
cp $REPRO/TestAddIndexesNoMergeScheduler.java $LUCENE/lucene/core/src/test/org/apache/lucene/index/
cd $LUCENE && ./gradlew :lucene:core:test --tests org.apache.lucene.index.TestAddIndexesNoMergeScheduler -Ptests.seed=DEADBEEF
# takes about 10 s to fail; assertions off fails the same way: -Ptests.asserts=false (the recorded runs added --offline)
# gradle, stock 645d4fe14a20, -Ptests.seed=DEADBEEF (stack trimmed)
TestAddIndexesNoMergeScheduler > testAddIndexesCodecReaderReturns FAILED
java.lang.AssertionError: addIndexes must return or throw, but was still blocked after 10 s
at __randomizedtesting.SeedInfo.seed([DEADBEEF:4385ADD63B8A706A]:0)
at org.apache.lucene.index.TestAddIndexesNoMergeScheduler.testAddIndexesCodecReaderReturns(TestAddIndexesNoMergeScheduler.java:53)
# thread dump while blocked (main 645d4fe14a20, seed 1234ABCD, same failure)
"Thread-0" #41 [16675] prio=5 os_prio=0 cpu=3.02ms elapsed=4.17s ... waiting on condition
java.lang.Thread.State: WAITING (parking)
- parking to wait for <0x00000000f5d23bb8> (a java.util.concurrent.CompletableFuture$Signaller)
at java.util.concurrent.CompletableFuture.get(java.base@25.0.3/CompletableFuture.java:2093)
at org.apache.lucene.index.MergePolicy$MergeSpecification.await(MergePolicy.java:537)
at org.apache.lucene.index.IndexWriter.addIndexes(IndexWriter.java:3313)
# standalone runner, seed DEADBEEF: main 645d4fe14a20 fails with -ea and -da (about 11.4 s);
# releases 10.6.0 and 9.4.0 fail; 9.3.0 passes (whole run 1.3 s)NoMergePolicykeeps this merge on purpose "to enable addIndexes", and theNoMergeSchedulerjavadoc recommends using both.- No javadoc says the call may block; where Lucene handles a scheduler that will not run a merge explicitly, it throws.
- 9.3.0 returns and adds the readers; 9.4.0 hangs.
The full argument213 words
- Kept working on purpose: LUCENE-10216 added the
NoMergePolicy.findMerges(CodecReader...)override that keeps the merge "to enable addIndexes for consumers like IndexRearranger" (NoMergePolicy.java:43-46); the PR author wrote on #633 that "There are consumers of NoMergePolicy that expect addIndexes(CodecReaders...) to work".NoMergeScheduler's javadoc: "it is recommended to use both if you want to disable merges from ever happening" (NoMergeScheduler.java:26-30). - No blocking in the contract: the
addIndexes(CodecReader...)javadoc (IndexWriter.java:3254-3274) describes merging throughMergePolicy#findMerges(CodecReader...)and lists onlyCorruptIndexException,IOExceptionandIllegalArgumentException. - Where Lucene handles it explicitly, it throws:
NoMergeScheduler.getIntraMergeExecutorthrowsUnsupportedOperationException("NoMergeScheduler does not support merges")(NoMergeScheduler.java:57-60). The upstream test scheduler that skips addIndexes merges,TestAddIndexes.PartialMergeScheduler, closes each skipped merge (TestAddIndexes.java:759-790), and its test expectsaddIndexesto throw (:807-808). These are single cases, not a general contract, and not every case is handled: in a sibling report,close(),forceMerge(1)andforceMergeDeletes()wait forever withNoMergeScheduler, known since #5316 (2014) and still unfixed. - A regression: up to 9.3.0 the method merged inline and its javadoc said "this method does not call or make use of the MergeScheduler" (9.3.0
IndexWriter.java:3124-3125). LUCENE-10216's issue, PR and reviews never discuss a scheduler that runs no merges. - Tests: no upstream test combines
NoMergeSchedulerwithaddIndexes.
The devil's-advocate check170 words
The strongest case for "intended" (a read-only cross-model review rated it strong; the second cross-model review and the adjudication, moderate): the configuration contradicts itself. NoMergeScheduler "never executes any merges ... regardless of the MergePolicy used"; since 9.4.0 the CHANGES entry for LUCENE-10216 says the MergeScheduler triggers the merges "required by addIndexes(CodecReader[])", and MergePolicy.findMerges(CodecReader...) calls that merge required (MergePolicy.java:619). A user who disables every merge and asks for a merge-based operation gets no readers added.
Why it fails: that justifies at most not adding the readers, not a silent wait with no time limit that a single-threaded caller cannot leave. Where Lucene handles a scheduler that will not run a merge explicitly, it throws (getIntraMergeExecutor, PartialMergeScheduler), Lucene deliberately kept this method working for the "no merges" policy, and nothing in the javadoc of the recommended pair hints that it breaks this method. Hence the narrow claim: return or throw. Note that the 9.4.0 hand-off itself was announced in CHANGES; only its consequence for NoMergeScheduler was not.
Recommended Fail fast in addIndexes(CodecReader...) when the scheduler is NoMergeScheduler (validated)
Fail fast: in addIndexes(CodecReader...), throw IllegalStateException when the scheduler is NoMergeScheduler, before any merge is registered (+7 lines in IndexWriter.java). The call then throws at once and adds nothing. It passes the repro, TestAddIndexes, TestNoMergeScheduler and TestNoMergePolicy (51 tests).
if (spec != null && spec.merges.size() > 0) {
if (mergeScheduler == NoMergeScheduler.INSTANCE) {
// the readers are added through merges, which NoMergeScheduler never runs: fail fast
// instead of waiting forever in spec.await()
throw new IllegalStateException(
"addIndexes(CodecReader...) adds the readers through merges, but the configured"
+ " NoMergeScheduler never runs merges");
}
try {
spec.merges.forEach(addIndexesMergeSource::registerMerge);All options and trade-offs4 options · 533 words
One fix was validated through gradle in a scratch worktree of 645d4fe14a20; a second (run the merge inline) passed the hunt's own check; a javadoc note is a complement. A generic "merges still queued" check was considered and rejected.
Fail fast in addIndexes(CodecReader...) when the scheduler is NoMergeScheduler (validated) (recommended)
Check the scheduler right after the merge policy returns a non-empty spec and before registerMerge, so nothing is queued and no merge output is written (the call's initial flush still runs first, as today).
if (spec != null && spec.merges.size() > 0) {
if (mergeScheduler == NoMergeScheduler.INSTANCE) {
// the readers are added through merges, which NoMergeScheduler never runs: fail fast
// instead of waiting forever in spec.await()
throw new IllegalStateException(
"addIndexes(CodecReader...) adds the readers through merges, but the configured"
+ " NoMergeScheduler never runs merges");
}
try {
spec.merges.forEach(addIndexesMergeSource::registerMerge);Trade-offs
- Only calls that hang today change; a
nullor empty spec still returns early (IndexWriter.java:3325-3338).NoMergeScheduleris a final singleton with a private constructor, so the identity check is exact. - The readers are still not added: users who want "no merges, but addIndexes works" switch to a merge-running scheduler with
NoMergePolicy(works today) or toaddIndexes(Directory...); the message could say so. - A custom scheduler that silently drops merges still hangs; that scheduler breaks the
MergeSchedulercontract ("Run the merges provided by..."). IllegalStateExceptionvsUnsupportedOperationException(asgetIntraMergeExecutorthrows) is the maintainers' choice.
What the checks validated
- Gradle, scratch worktree of
645d4fe14a20, OpenJDK 25.0.3, seed DEADBEEF (2026-10-31): stock, the minimal test fails. With the fix: the minimal test passes (well under a second) with assertions on and off;TestAddIndexes39/39,TestNoMergeScheduler3/3 andTestNoMergePolicy8/8 pass: 51 tests, 0 failures. Formatting was not checked. - The standalone runner gave the same pass on
645d4fe14a20with assertions on and off, and the hunt's check ona9779ef3c193(the call throws within the bound, nothing added).
Let NoMergeScheduler run ADD_INDEXES merges in the calling thread (alternative) (validated by the checks)
In NoMergeScheduler.merge, when the trigger is MergeTrigger.ADD_INDEXES, loop getNextMerge() / mergeSource.merge(merge); getIntraMergeExecutor returns Runnable::run instead of throwing.
Trade-offs
Restores the observed add-and-return outcome of 9.3.0 (the readers are added, through today's scheduler path rather than 9.3.0's inline merge), matching the intent of the NoMergePolicy override. But it breaks the class's documented promise ("never executes any merges") and needs a javadoc change, and TestNoMergeScheduler calls merge(null, random trigger), which would throw NullPointerException when the random trigger is ADD_INDEXES, so it needs a null guard.
What the checks validated
The hunt's check on a9779ef3c193 passed: the call returned with no exception and added the document. Not run through gradle or the module's tests.
A note on addIndexes(CodecReader...) and on NoMergeScheduler.
Trade-offs
Alone it leaves the hang in place; useful together with either fix.
Generic, so it would also catch custom schedulers.
Trade-offs
Wrong for legal schedulers: ConcurrentMergeScheduler.merge stops pulling merges when maybeStall returns false (ConcurrentMergeScheduler.java:564-568; subclasses may override maybeStall, :599-609) and picks queued merges up later when a merge thread finishes (:678-686). Not run.
Downstream, the risk is a stuck index-administration thread in a service that pairs NoMergeScheduler with addIndexes(CodecReader...); a read-only, non-exhaustive code search found Elasticsearch pairing it with the unaffected addIndexes(Directory...), found that Solr cannot configure NoMergeScheduler, and found no use in OpenSearch.
A TLA+ model of IndexWriter's forceMerge wait protocol proved "no caller waits forever" only under the assumption that some merge thread eventually takes every pending merge. Checking that assumption against Lucene's shipped schedulers found NoMergeScheduler, which takes none; following it into addIndexes(CodecReader...) gave this bug, confirmed by a JUnit on the stock jar and a release bisection.
The method in full161 words
Method: semi-formal (an assumption of a TLA+ liveness proof checked against the code, then a public-API JUnit).
- Model.
ForceMergeWait.tlamodels callers waiting for forced merges and merge threads running them. Its property F3, "no caller waits forever" (<>(cpc[c] \in {"ok", "threw"})for every caller), holds under a fairness assumption on merge threads (TLC: 73,524 and 916,793 states for one and two callers). The model does not coveraddIndexes. - Signal. Checking that fairness assumption against the shipped schedulers:
NoMergeScheduler.mergetakes no merge at all. Two code paths wait on such merges:close()/forceMerge(a separate, already known report) and, since 9.4.0,addIndexes(CodecReader...)(this bug). - Confirmation. A JUnit on the stock main jar blocked with assertions on and off; a release bisection showed 9.3.0 returns and 9.4.0 hangs. An independent reproducer, a devil's-advocate verifier, read-only cross-model reviews (OpenAI GPT-5.6), an adjudication and a credibility audit checked it; a follow-up ran the version history, a prior-report search and a security screen.
No prior report found (195 logged queries on 2026-10-26 over GitHub, JIRA, the mailing lists and the web; rechecked 2026-10-31). Closest: 2014 comments on #5316 about close() waiting forever with NoMergeScheduler, a different code path whose workaround (NoMergePolicy) still hangs here.
Searches3 searches
Verdict new: no issue, PR, JIRA entry, list thread or downstream issue reports addIndexes(CodecReader...) blocking with NoMergeScheduler, or discusses the two together; no open PR fixes it. The #5316 comments (LUCENE-4246, 2014) describe the sibling problem in close(), eight years before this code path existed.
| Where | When | How | Found |
|---|---|---|---|
| GitHub issues, PRs and commits (apache/lucene and downstream search engines), JIRA, mailing lists, web | 2026-10-26 | a scripted prior-report search (110 queries), 76 GitHub GraphQL searches, full reads of LUCENE-10216 (#11253, #633 with all 52 inline review comments and 39 reviews, #1051) and 16 other items, a JIRA search for NoMergeScheduler across all ASF projects (7 issues), mailing-list threads, the touched files of all 406 open apache/lucene PRs | no report; closest #5316 (theme only); related #13124 / #13190 (getIntraMergeExecutor throws), #14993, #14994, #15120 (other AddIndexesMergeSource defects), SOLR-11581 (Solr cannot configure NoMergeScheduler) |
| Verifiers' own searches | 2026-10-26 | two scripted prior-report search runs (45 and 40 queries) | no report |
| GitHub (recheck) | 2026-10-31 | NoMergeScheduler in apache/lucene issues and PRs; issues and PRs mentioning addIndexes created since 2026-10-26 | the same 4 items from 2010-2014 (#3407, #3452, #3460, #5316); nothing new |
Introduced in 9.4.0 by LUCENE-10216 (#633 on main, 698f40ad51a, 2022-07-06; backport #1051, 5dd8e9bdc5a), which made addIndexes(CodecReader...) hand its merge to the scheduler and wait. Both commits hang and their parents return; 9.3.0 returns, and every release from 9.4.0 to 10.6.0 and main hangs.
Versions tested6 rows
Introduced by LUCENE-10216 (#11253; commit and PR title "Use MergeScheduler and MergePolicy to run addIndexes(CodecReader[]) merges"; Vigya Sharma): main 698f40ad51a (#633, merged 2022-07-06) and branch_9x backport 5dd8e9bdc5a (#1051, merged 2022-08-01), first released in 9.4.0. It replaced the inline SegmentMerger merge with the scheduler hand-off and spec.await(), removed the javadoc note "this method does not call or make use of the MergeScheduler" and added the NoMergePolicy.findMerges(CodecReader...) override. The 9.4.0 CHANGES entry announces the hand-off; it does not mention NoMergeScheduler, whose merge has been empty since 2010. No later commit changes the hand-off.
| Version or commit | Verdict | Result |
|---|---|---|
main 645d4fe14a20; 10.6.0 | affected | minimal repro: still blocked after 10 s (main with assertions on and off) |
9.4.0 to 10.5.2 (29 releases); main a9779ef3c193 | affected | finder's JUnit: no return within 10 s, released only by an interrupt; the minimal repro also fails on 9.4.0 |
698f40ad51a (#633), 5dd8e9bdc5a (#1051) | commit | core-only probe hangs in MergeSpecification.await |
parents d537013e7087, 18f839bbf408 | commit | returns, 15 -> 16 docs |
| 9.1.0, 9.2.0, 9.3.0 | not affected | returns and adds the readers (9.3.0 also with the minimal repro) |
| 8.11.4, 9.0.0 | not affected | core-only probe returns |
An independent reproducer, a devil's-advocate verifier, an adjudication and a credibility audit all reproduced it on stock builds; a read-only cross-model review (OpenAI GPT-5.6) agreed, and a second returned a procedural "not real" because it could not run code. This doc's gradle run reproduced it and validated the fix.
Checks and verdicts277 words
Every executed check ran on stock builds; the cross-model reviewers could only read code, and separate runs re-ran the repro.
| Check | Verdict | What it found |
|---|---|---|
| Finder (JUnit, release bisection) | candidate | JUnit (15 committed docs, 10 s bound, SerialMergeScheduler control) fails on main with assertions on and off, 10.5.2 and 9.4.0; passes on 9.3.0; both fix sketches pass. |
| cross-model review (OpenAI GPT-5.6, refute lens) | not real (procedural, low) | Read-only, could not run code; agreed with the root cause and that the inputs are legal; rated the self-contradictory-configuration case strong and found the run-inline fix's NPE. Its points were adopted. |
| Reproducer (own JUnit) | real | 5 tests: both NoMergeScheduler cases (with NoMergePolicy and with TieredMergePolicy) block on main with assertions on and off, 10.5.2, 9.12.3 and 9.4.0; pass on 9.3.0; controls pass; rollback() releases the call with MergeAbortedException. |
| Cross-model review (OpenAI GPT-5.6, intended lens) | real, medium | Read-only; a separate run re-ran the finder's JUnit on main: fails. |
| Devil's advocate | real, medium-high | Reproduced through Lucene's benchmark module (CreateIndexTask with NoMergeScheduler, then AddIndexes(false)) on main with assertions on and off; returns on 9.3.0; corrected the "unannounced" wording. |
| Adjudication | confirmed, high | Re-ran the independent reproduction's JUnit with a new seed on main, assertions on and off: 2 of 5 fail, controls pass. |
| Credibility audit | keep | A new minimal JUnit blocks for more than 20 s with assertions on and off; close() from another thread releases it with MergeAbortedException. |
| This doc | reproduced, fix validated | Minimal repro through gradle on 645d4fe14a20; 10.6.0 and 9.4.0 fail and 9.3.0 passes; the fail-fast fix passes the repro and 51 tests of the closest suites. |
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 476 KB
Finding record, Lucene and upstream sources, repro files and diagram notes1,151 words
- Registry title
- Since 9.4.0, IndexWriter.addIndexes(CodecReader...) blocks indefinitely (until another thread interrupts it or rolls the writer back) when the writer uses NoMergeScheduler, also together with NoMergePolicy as the NoMergeScheduler javadoc recommends: the reader merge is handed to the merge scheduler, which never runs it, and is then awaited with no time limit
- Area
- index / IndexWriter addIndexes(CodecReader...) (merge-scheduler hand-off, NoMergeScheduler)
- Priority
- Low, rank 797: Since 9.4, addIndexes(CodecReader...) under NoMergeScheduler blocks on every call, but nothing is damaged and the pairing is unusual, so it ends the new hang rows before LFV-208.
- Impact class
- hang
- Impact note
- Hits applications that configure NoMergeScheduler (a non-default Expert setting; alone, with the default TieredMergePolicy, or paired with NoMergePolicy as its javadoc recommends) and call addIndexes(CodecReader...), for example to copy, filter or rearrange segments into a writer whose merges are disabled; every such call blocks, deterministically, with -ea and -da and at any size; the calling thread parks in an unbounded wait without CPU use until another thread interrupts it (ThreadInterruptedException) or calls rollback() (MergeAbortedException), so a single-threaded caller never gets control back; the readers are not added and nothing is written or corrupted; addIndexes(Directory...) and every merge-running scheduler are unaffected (executed controls).
- Summary
- You configure an IndexWriter with NoMergeScheduler (also with NoMergePolicy, the pairing the NoMergeScheduler javadoc recommends) and call addIndexes(CodecReader...). You expect the call to add the readers, as it did up to 9.3.0, or at least to return or throw. Instead, since 9.4.0 (LUCENE-10216) the call hands its reader merge to the merge scheduler and then waits for it with no time limit; NoMergeScheduler.merge does nothing, so the call blocks until another thread interrupts it or rolls the writer back.
- Introduced
- 698f40ad51af0c42b0a4a8321ab89968e8d0860b (main): apache/lucene#633, LUCENE-10216 'Use MergeScheduler and MergePolicy to run addIndexes(CodecReader[]) merges' (issue #11253; author vigyasharma; merged 2022-07-06), and its branch_9x backport 5dd8e9bdc5ae72fc726a98a64bfce5119c77b558 (#1051, merged 2022-08-01), which shipped in 9.4.0. They replaced the inline SegmentMerger merge with registerMerge + mergeScheduler.merge(addIndexesMergeSource, ADD_INDEXES) + spec.await() and removed the javadoc note 'this method does not call or make use of the MergeScheduler' [exec at commit level: 698f40ad51a and 5dd8e9bdc5a hang, their parents d537013e7087 and 18f839bbf408 return; read: git blame of IndexWriter.java:3306-3315 on a9779ef3c193 and the only commit between the 9.3.0 and 9.4.0 tags touching the involved files]. NoMergeScheduler.merge has been empty since the class was added (c8c5dfa5431, LUCENE-2331, 2010). (2022-07-06 (main); 2022-08-01 (branch_9x backport); first release 9.4.0 (tag dated 2022-09-30))
- First affected version
- 9.4.0 [exec: 9.3.0 returns and adds the readers, 9.4.0 blocks; same at commit level]
- Affected versions
- every release from 9.4.0 through 10.5.2 (29 releases: 9.4.0 to 9.12.3 and 10.0.0 to 10.5.2) and main a9779ef3c193, -ea and -da [exec, follow-up release sweep]; the previous hunt main ea57360912ed too [read: the 7 commits between the two mains touch none of IndexWriter, MergePolicy, NoMergePolicy, MergeScheduler or NoMergeScheduler]. Not affected [exec]: 9.1.0, 9.2.0 and 9.3.0 (candidate JUnit), 9.0.0 and 8.11.4 (core-only probe); older releases predate the scheduler hand-off [read].
- Status
- documented (brief, checks and a per-finding doc exist)
- Verification
- Confirmed by AI-run checks: an independent repro, a cross-model review that could only read the code (its low-confidence “not real” is procedural), an intended-behaviour check, a prior-report search and a skeptical audit; not yet triaged by a person.
- Found by
- semi-formal
- Upstream and prior reports
- none found (2026-10-26, read-only; the prior-report record, verdict new; 195 queries, 25 hits recorded, none the same bug): a scripted prior-report search (110 queries over apache/lucene issues, PRs and commits, downstream repositories, the dev@, java-user@, issues@ and commits@ lists, JIRA and the web), 76 GitHub GraphQL searches, full reads of LUCENE-10216 (#11253, #633 with all inline review comments and reviews, #1051) and 16 other apache/lucene items, term searches of 5 more (including the 67 comments of #5316) and of 11 downstream items, a JIRA text search for NoMergeScheduler across ASF projects (7 issues), mailing-list threads, code search, and the touched files of all 406 open apache/lucene PRs (none fixes it); the independent reproduction's (45 queries) and the intended-behaviour check's (40 queries) scripted prior-report searches agree. Related, theme only: apache/lucene#5316 (LUCENE-4246), 2014 comments that close() waited forever with NoMergeScheduler (LFV-587); that is a different API and code path, 8 years before this one existed, and its workaround (NoMergePolicy) still hangs here. Not fixed on main.
- Lucene commit tested
- main
645d4fe14a20(2026-10-28): the minimal test fails through gradle in a scratch worktree and through a standalone runner with assertions on and off; with the fail-fast fix it passes, as doTestAddIndexes,TestNoMergeSchedulerandTestNoMergePolicy(51 tests, 0 failures). Releases 10.6.0 and 9.4.0 fail the minimal test and 9.3.0 passes (standalone runner). The hunt ran on maina9779ef3c193(the involved files are identical) and swept the releases 8.11.4 to 10.5.2. Upstream main2f424fd428d(2026-10-30),branch_11x603af870375andbranch_10_67e5aec4370ecarry the same code (read from the source).
- Finding record
lucene/core/src/java/org/apache/lucene/index/IndexWriter.java:3275 (at a9779ef3c193) (addIndexes(CodecReader...)), :3306-3313 (findMerges(readers); spec.merges.forEach(addIndexesMergeSource::registerMerge); mergeScheduler.merge(addIndexesMergeSource, MergeTrigger.ADD_INDEXES); spec.await())lucene/core/src/java/org/apache/lucene/index/NoMergeScheduler.java:23-31 (at a9779ef3c193) (javadoc: 'never executes any merges', 'regardless of the MergePolicy used', 'recommended to use both'), :45 (merge(...) is empty), :57-60 (getIntraMergeExecutor throws UnsupportedOperationException 'NoMergeScheduler does not support merges')lucene/core/src/java/org/apache/lucene/index/MergePolicy.java:533-544 (at a9779ef3c193) (MergeSpecification.await: 'Waits, until interrupted', unbounded future.get())lucene/core/src/java/org/apache/lucene/index/NoMergePolicy.java:42-50 (at a9779ef3c193) (findMerges(CodecReader...) keeps the default 'to enable addIndexes for consumers like IndexRearranger', LUCENE-10216)lucene/benchmark/src/java/org/apache/lucene/benchmark/byTask/tasks/CreateIndexTask.java:110-113 (at a9779ef3c193) (merge.scheduler=NoMergeScheduler accepted), AddIndexesTask.java:61-72 (AddIndexes(false) calls addIndexes(CodecReader...)); lucene/benchmark/conf/addIndexes.alg:41lucene/misc/src/java/org/apache/lucene/misc/index/IndexRearranger.java:119-124 (at a9779ef3c193) (fresh IndexWriterConfigs, NoMergePolicy, default scheduler); PKIndexSplitter.java:67-68, :117, :129 (caller-supplied IndexWriterConfig, addIndexes(CodecReader...))
- The finding in 30 seconds
a Lucene main checkout (645d4fe14a20) lucene/core/src/java/org/apache/lucene/index/IndexWriter.java:3306-3313 (hand-off and wait), :3408-3417 (registerMerge), :2821-2837 and :3443-3454 (abortMerges, abortPendingMerges), :2621 (rollbackInternal -> abortMerges), :2743-2745 and :2771 (deleteAll javadoc and abortMerges call), :3298-3304 (synchronized block before the hand-off), :3376-3378 (MergeAbortedException)a Lucene main checkout (645d4fe14a20) lucene/core/src/java/org/apache/lucene/index/MergePolicy.java:533-557 (await, unbounded and bounded), :615-631 (findMerges(CodecReader...); TieredMergePolicy does not override it)a Lucene main checkout (645d4fe14a20) lucene/core/src/java/org/apache/lucene/index/NoMergeScheduler.java:23-31 (javadoc), :45 (empty merge); NoMergePolicy.java:42-50; MergeScheduler.java:43-49
- Who is affected
a Lucene main checkout (645d4fe14a20) lucene/core/src/java/org/apache/lucene/index/IndexWriterConfig.java:271-279a Lucene main checkout (645d4fe14a20) lucene/benchmark/src/java/org/apache/lucene/benchmark/byTask/tasks/CreateIndexTask.java:110-113, AddIndexesTask.java:72; lucene/benchmark/conf/addIndexes.alg:41a Lucene main checkout (645d4fe14a20) lucene/misc/src/java/org/apache/lucene/misc/index/IndexRearranger.java:119-123; PKIndexSplitter.java:67-68, :117, :129a Lucene main checkout (645d4fe14a20) lucene/core/src/java/org/apache/lucene/index/IndexWriter.java:3286 (flush before the hand-off), :3325-3338 (null or empty spec returns early)
- Why it looks unintended
a Lucene main checkout (645d4fe14a20) lucene/core/src/java/org/apache/lucene/index/NoMergePolicy.java:42-50; NoMergeScheduler.java:23-31, :57-60; MergePolicy.java:615-631; IndexWriter.java:3254-3274a Lucene main checkout (645d4fe14a20) lucene/core/src/test/org/apache/lucene/index/TestAddIndexes.java:759-790, :807-808a Lucene clone releases/lucene/9.3.0:lucene/core/src/java/org/apache/lucene/index/IndexWriter.java:3124-3125; releases/lucene/10.6.0:lucene/CHANGES.txt:3553-3554 (9.4.0 section)
- Suggested fix
a Lucene main checkout (645d4fe14a20) lucene/core/src/test/org/apache/lucene/index/TestNoMergeScheduler.java:28-32a Lucene main checkout (645d4fe14a20) lucene/core/src/java/org/apache/lucene/index/ConcurrentMergeScheduler.java:564-568, :599-609, :678-686; IndexWriter.java:3325-3338; NoMergeScheduler.java:32-39; MergeScheduler.java:43-49
- History
a Lucene clone origin/main 2f424fd428d, origin/branch_11x 603af870375, origin/branch_10_6 7e5aec4370e (same hand-off and empty NoMergeScheduler.merge) [read]
- TestAddIndexesNoMergeScheduler.java (one test class):
repro/LFV-586/src/org/apache/lucene/index/TestAddIndexesNoMergeScheduler.java(57 lines)
Made in parallel with this doc from the code at 645d4fe14a20 and the minimal repro's output.
assets/LFV-586/bug.png(476 KB)
