One refresh, two failures: the taxonomy reopen throws IndexNotFoundException, then closing the new index reader throws an IOException. The caller should get the IndexNotFoundException; it gets IllegalArgumentException: Self-suppression not permitted.
SearcherTaxonomyManagerkeeps anIndexSearcherand aTaxonomyReader, the facet module's dictionary of category labels, at matching points in time.maybeRefresh()moves both to newer commits; its javadoc says it throwsIOExceptionwhen refreshing fails.- A refresh reopens the index reader first, then the taxonomy reader. If the taxonomy reopen fails, the code closes the index reader it just opened (so its files do not leak) and rethrows the taxonomy error.
- Java's
a.addSuppressed(b)attaches a secondary failurebto the primary failurea. A throwable cannot suppress itself:b.addSuppressed(b)always throwsIllegalArgumentException("Self-suppression not permitted").
t2.addSuppressed(t2) replaces both with "Self-suppression not permitted". Select the still to open it at full size. Generated with AI, checked against the code.- Keep a searcher and its facet taxonomy in step with
SearcherTaxonomyManagerand callmaybeRefreshBlocking()after new commits. Each refresh reopens the index reader first, then the taxonomy reader. - One refresh fails twice. The taxonomy reopen throws
IndexNotFoundException(in the repro its newest commit file is deleted, as in the upstream testtestExceptionDuringRefresh). Then closing the index reader just reopened fails: every file close inside it throws an injectedIOException(two in the test run), and Lucene'sIOUtils.closekeeps the first and suppresses the rest, so the manager sees one close failure. - You expect the documented
IOException: the taxonomy'sIndexNotFoundException, with the close failure attached to it as a suppressed exception. - Instead you get an unchecked
IllegalArgumentException: Self-suppression not permitted, caused by the close failure. TheIndexNotFoundExceptionis in neither the cause nor the suppressed list: the cleanup callst2.addSuppressed(t2), which Java always rejects, sothrow t1never runs. - Nothing leaks and, once the faults are gone, the next refresh works; but
catch (IOException e)misses it: a refresh task scheduled every 5 ms that catches onlyIOExceptionis cancelled after this 1 refresh, and its searcher then stays stale; with only the taxonomy failure it catches theIOExceptionand keeps running (93 runs in the test).
Applications that use taxonomy facets through SearcherTaxonomyManager (the opt-in facet module), 7.4.0 to 10.6.0 and main, when one refresh suffers two failures at once, in practice two I/O errors. The root cause is lost, and code catching the documented IOException misses the unchecked exception. Nothing leaks and the index is not damaged.
Mechanism, terms and numbers421 words
SearcherTaxonomyManager refreshes an IndexSearcher and its facet TaxonomyReader together: it reopens the index reader, then the taxonomy reader. If the taxonomy reopen throws, it closes the index reader it just opened and rethrows the taxonomy error, a cleanup added in 7.4.0 by LUCENE-8244 to stop a file-handle leak. When that close also throws, the cleanup calls t2.addSuppressed(t2) (SearcherTaxonomyManager.java:209) where t1.addSuppressed(t2) was meant. Java rejects self-suppression with IllegalArgumentException("Self-suppression not permitted"), so maybeRefresh() and maybeRefreshBlocking() throw that unchecked exception, caused by the close failure, instead of the documented IOException, and the taxonomy failure is lost from the whole chain. Nothing leaks, the index is not damaged and, once the faults are gone, the next refresh works; but code that catches only IOException misses it, and a scheduled refresh task that does so is cancelled, leaving its searcher stale. 7.3.1 passes, 7.4.0 fails and 7.4.0 with the one-token fix passes; every release executed (7.4.0, 7.6.0, 7.7.3, 8.0.0, 8.11.4, 9.0.0, 9.12.3, 10.0.0, 10.5.2) fails, and 10.6.0 and main have the same line. Low priority: it needs two failures in one refresh.
// SearcherTaxonomyManager.refreshIfNeeded, condensed
IndexReader newReader = DirectoryReader.openIfChanged(dr, refreshCommit); // :195 index first
...
try {
tr = TaxonomyReader.openIfChanged(ref.taxonomyReader); // :204 taxonomy: throws t1
} catch (Throwable t1) {
try {
IOUtils.close(newReader); // :207 close: throws t2
} catch (Throwable t2) {
t2.addSuppressed(t2); // :209 always throws
}
throw t1; // :211 skipped
}ReferenceManager.doMaybeRefreshcallsrefreshIfNeeded(ReferenceManager.java:167). Line 195 reopens the index reader; line 204 reopens the taxonomy reader, which throwst1.- Line 207 closes the new index reader. When that throws
t2, line 209 callst2.addSuppressed(t2). Throwable.addSuppressedstarts withif (exception == this) throw new IllegalArgumentException(SELF_SUPPRESSION_MESSAGE, exception);(JDKThrowable.java:1099-1101; javadoc: "a throwable cannot suppress itself", since Java 7). So line 209 always throws, witht2as the cause.- That exception leaves the inner
catch, sothrow t1is skipped andt1is dropped. It is unchecked, so it passesrefreshIfNeeded'sthrows IOExceptionand comes out ofmaybeRefresh/maybeRefreshBlocking. doMaybeRefresh'sfinallyblocks release the current reference, notify listeners and unlock (ReferenceManager.java:180-187): the manager is unchanged and usable.
What the caller gets on main, with assertions on and off:
java.lang.IllegalArgumentException: Self-suppression not permitted
at java.base/java.lang.Throwable.addSuppressed(Throwable.java:1101)
at org.apache.lucene.facet.taxonomy.SearcherTaxonomyManager.refreshIfNeeded(SearcherTaxonomyManager.java:209)
...
Caused by: java.io.IOException: simulated close failure <- the close failure (t2)
...
<- IndexNotFoundException (t1): nowhere(The arrows are ours.) A search of Lucene's Java sources for the same-identifier pattern x.addSuppressed(x) finds no other place.
Exposure and likelihood333 words
- Who. Applications that use taxonomy facets through
SearcherTaxonomyManager, with any of its constructors, on 7.4.0 through 10.6.0 and main. - When. Only when one refresh suffers two failures at once: the taxonomy reopen throws (an I/O error, an unreadable taxonomy commit, or any other
Throwable) and closing the index segment files opened moments earlier also throws. Each is an ordinary environmental fault, but the second is uncommon on its own, so the pair is rare; the recorded searches found no report of it. - What goes wrong. An unchecked
IllegalArgumentExceptioninstead of the documentedIOException, and the root cause, the taxonomy failure, is lost from logs and diagnostics; the message points at the JDK, not at the taxonomy. - Code that catches
IOExceptionmisses it (executed on main with assertions on and off, 10.5.2 and 9.12.3): aScheduledExecutorServicetask that callsmaybeRefresh()every 5 ms and catches onlyIOExceptionis cancelled by the JDK after the double-failure refresh (1 tick); in that setup, where nothing inspects the task'sScheduledFuture, nothing is logged, and a document deleted in the new commit is still returned until the application refreshes by hand. With only the taxonomy failure, the task catches oneIOExceptionand keeps running (93 ticks); with the fix it keeps running too (77 ticks). - Lucene's own
ControlledRealTimeReopenThreadends on any refreshIOException(it wraps it in aRuntimeException), so for it only the exception type and message change. - By reading only: if the taxonomy failure is an
Error, such as anOutOfMemoryError, and the close also fails, the caller gets theIllegalArgumentExceptioninstead of theError. - What it does not do: no leak (the new reader's files are closed) and no index damage. The failed refresh keeps the previous searcher, as any failed refresh does, and once the faults are gone a later refresh succeeds (executed with transient faults). Results go stale only while nothing refreshes, for example after the application's refresh task died on the unchecked exception (above).
One JUnit test, public API plus the test framework's MockDirectoryWrapper: it deletes the taxonomy's newest segments_N, as the upstream testExceptionDuringRefresh does, makes file closes inside SegmentReader.doClose throw an IOException, and expects the taxonomy's IndexNotFoundException from maybeRefreshBlocking().
/*
* 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.facet.taxonomy;
import java.io.IOException;
import org.apache.lucene.document.Document;
import org.apache.lucene.facet.taxonomy.directory.DirectoryTaxonomyWriter;
import org.apache.lucene.index.IndexNotFoundException;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.SegmentInfos;
import org.apache.lucene.index.SegmentReader;
import org.apache.lucene.store.Directory;
import org.apache.lucene.tests.store.MockDirectoryWrapper;
import org.apache.lucene.tests.util.LuceneTestCase;
import org.apache.lucene.util.IOUtils;
public class TestSearcherTaxonomyManagerDoubleFailure extends LuceneTestCase {
public void testTaxonomyFailureSurvivesReaderCloseFailure() throws Exception {
MockDirectoryWrapper indexDir = newMockDirectory();
Directory taxoDir = newDirectory();
IndexWriter w = new IndexWriter(indexDir, newIndexWriterConfig());
DirectoryTaxonomyWriter tw = new DirectoryTaxonomyWriter(taxoDir);
w.commit();
tw.commit();
SearcherTaxonomyManager mgr = new SearcherTaxonomyManager(indexDir, taxoDir, null);
tw.addCategory(new FacetLabel("a", "b"));
w.addDocument(new Document());
tw.commit();
w.commit();
taxoDir.deleteFile(SegmentInfos.readLatestCommit(taxoDir).getSegmentsFileName());
MockDirectoryWrapper.Failure closeFails =
new MockDirectoryWrapper.Failure() {
@Override
public void eval(MockDirectoryWrapper dir) throws IOException {
if (doFail && callStackContains(SegmentReader.class, "doClose")) {
throw new IOException("simulated close failure");
}
}
};
indexDir.failOn(closeFails);
closeFails.setDoFail();
// expected: the taxonomy's IndexNotFoundException; actual: IllegalArgumentException (self-suppression)
expectThrows(IndexNotFoundException.class, mgr::maybeRefreshBlocking);
closeFails.clearDoFail();
IOUtils.close(w, tw, mgr, indexDir, taxoDir);
}
}
cp $REPRO/TestSearcherTaxonomyManagerDoubleFailure.java $LUCENE/lucene/facet/src/test/org/apache/lucene/facet/taxonomy/
cd $LUCENE && ./gradlew :lucene:facet:test --tests org.apache.lucene.facet.taxonomy.TestSearcherTaxonomyManagerDoubleFailure -Ptests.seed=DEADBEEF -Ptests.asserts=true
# same failure with -Ptests.asserts=false
- Expected
maybeRefreshBlocking()throws the taxonomy'sIndexNotFoundException, with the closeIOExceptionattached as suppressed.- Observed
IllegalArgumentException: Self-suppression not permitted, caused by the injected closeIOException; theIndexNotFoundExceptionappears nowhere (assertions on and off).
More files, environment, full output196 words
One test class, one method, single thread, nothing random that matters; the only subclass is the test framework's MockDirectoryWrapper.Failure, no reader or manager class. The close failure fires on every file close inside SegmentReader.doClose (two in the test run, which reach the manager as one close failure, the second suppressed in the first), which during maybeRefreshBlocking() runs only when the manager closes the index reader it just reopened after the taxonomy reopen failed. The failure is cleared before cleanup.
This same test is the suggested regression test: it fails on stock main and passes with both fixes.
| lucene | main 645d4fe14a20 (11.0.0-SNAPSHOT): fails with assertions on and off; with either fix: passes |
|---|---|
| jdk | OpenJDK 25.0.3 |
| os | Linux aarch64 |
# gradle, scratch worktree of 645d4fe14a20, seed DEADBEEF, -Ptests.asserts=true (module prefixes and some frames cut)
TestSearcherTaxonomyManagerDoubleFailure > testTaxonomyFailureSurvivesReaderCloseFailure FAILED
junit.framework.AssertionFailedError: Unexpected exception type, expected IndexNotFoundException but got java.lang.IllegalArgumentException: Self-suppression not permitted
at __randomizedtesting.SeedInfo.seed([DEADBEEF:C54840C3490FFE44]:0)
...
at org.apache.lucene.facet.taxonomy.TestSearcherTaxonomyManagerDoubleFailure.testTaxonomyFailureSurvivesReaderCloseFailure(TestSearcherTaxonomyManagerDoubleFailure.java:61)
Caused by:
java.lang.IllegalArgumentException: Self-suppression not permitted
at java.base/java.lang.Throwable.addSuppressed(Throwable.java:1101)
at org.apache.lucene.facet.taxonomy.SearcherTaxonomyManager.refreshIfNeeded(SearcherTaxonomyManager.java:209)
at org.apache.lucene.facet.taxonomy.SearcherTaxonomyManager.refreshIfNeeded(SearcherTaxonomyManager.java:40)
at org.apache.lucene.search.ReferenceManager.doMaybeRefresh(ReferenceManager.java:167)
at org.apache.lucene.search.ReferenceManager.maybeRefreshBlocking(ReferenceManager.java:240)
...
Caused by:
java.io.IOException: simulated close failure
at org.apache.lucene.facet.taxonomy.TestSearcherTaxonomyManagerDoubleFailure$1.eval(TestSearcherTaxonomyManagerDoubleFailure.java:53)
...
at org.apache.lucene.index.SegmentReader.doClose(SegmentReader.java:217)
...
at org.apache.lucene.facet.taxonomy.SearcherTaxonomyManager.refreshIfNeeded(SearcherTaxonomyManager.java:207)
... 7 more
1 test completed, 1 failedx.addSuppressed(x)can never succeed, and the block ends withthrow t1: the intent is to rethrow the taxonomy error with the close failure attached.maybeRefreshandmaybeRefreshBlockingdocument@throws IOException; the upstream test added with this code asserts the taxonomy'sIndexNotFoundExceptionfor the single-failure case.- The sibling
DirectoryTaxonomyReader.doOpenIfChangeddoes the same cleanup withIOUtils.closeWhileSuppressingExceptions(t, r2), which keeps the primary failure.
The full argument194 words
- The code states its intent. The block closes the new reader and then
throw t1(SearcherTaxonomyManager.java:211).t2.addSuppressed(t2)can never succeed (the JDK rejects it by contract), so no design calls it; the evident intent ist1.addSuppressed(t2). - The contract.
maybeRefreshandmaybeRefreshBlocking: "@throws IOException if refreshing the resource causes an IOException" (ReferenceManager.java:203,:231);refreshIfNeeded: "@throws IOException if the refresh operation failed" (:72). Nothing documents anIllegalArgumentException. - The test that came with this code.
TestSearcherTaxonomyManager.testExceptionDuringRefresh(:343-366), added by the same LUCENE-8244 commit, deletes the taxonomy's newestsegments_Nand assertsexpectThrows(IndexNotFoundException.class, mgr::maybeRefreshBlocking). Its close succeeds, and no upstream test makes that close fail. - The codebase idiom.
DirectoryTaxonomyReader.doOpenIfChangedhandles the same situation withcatch (Throwable t) { IOUtils.closeWhileSuppressingExceptions(t, r2); throw t; }(DirectoryTaxonomyReader.java:221-223). - History. The line came in the LUCENE-8244 patch ("Do not leak open file descriptors in SearcherTaxonomyManager's refresh on exception", 7.4.0 Bug Fixes); its issue (#9291) holds only the patch note and two commit notices (the JIRA record adds the 2022 migration notice), so nobody discussed this line there. Before that fix the taxonomy failure propagated (and the reader leaked).
The devil's-advocate check248 words
The strongest "works as intended or not worth it" case (the refute and intended-lens reviews and the devil's-advocate verifier): it needs two simultaneous failures; nothing leaks and the IllegalArgumentException still carries the close failure as its cause; the classes involved are @lucene.experimental: ReferenceManager (ReferenceManager.java:35), whose maybeRefresh javadoc is the contract quoted above, TaxonomyReader (TaxonomyReader.java:57) and DirectoryTaxonomyReader (DirectoryTaxonomyReader.java:61), while SearcherTaxonomyManager itself has no such tag; Java throws clauses are not exhaustive and no exception priority is documented for two failures at once; and the repro deletes an index file, while Lucene's SECURITY.md treats index files as trusted.
Why it fails: @lucene.experimental says the API may change between releases, not that a call which can never succeed is designed behaviour; none of this makes x.addSuppressed(x) meaningful, the block itself says throw t1, and the test that came with it asserts the taxonomy's exception. The deleted segments_N is the upstream test's own way to make the taxonomy reopen fail, and there Lucene asserts a clean IndexNotFoundException; an independent reproducer gets the same result with a plain injected IOException during DirectoryTaxonomyReader.doOpenIfChanged and no file deleted. Rarity and the unharmed state argue for Low priority, which is how it is classed, not for intent. The cross-model lenses rated the intended-behaviour argument weak; a later cross-model fact check rated it moderate, citing the stale searcher left by a dead refresh task, the experimental tags and fix B's Error handling, all three covered in this doc.
Recommended B. Use IOUtils.closeWhileSuppressingExceptions(t1, newReader), the module's idiom
Replace the hand-written cleanup with IOUtils.closeWhileSuppressingExceptions(t1, newReader); throw t1;, the idiom DirectoryTaxonomyReader.doOpenIfChanged already uses: the taxonomy error is rethrown with the close failure attached. The repro passes with assertions on and off, and the facet module's 307 tests run with 0 failures (7 skipped). The one-token t1.addSuppressed(t2) also works and suits backports.
} catch (Throwable t1) {
- try {
- IOUtils.close(newReader);
- } catch (Throwable t2) {
- t2.addSuppressed(t2);
- }
+ IOUtils.closeWhileSuppressingExceptions(t1, newReader);
throw t1;
}All options and trade-offs2 options · 377 words
Both options were run through gradle in a scratch worktree of 645d4fe14a20 and both fix the repro. B is recommended for main; A is the smallest change and the one for branches that lack the helper. Both change only the double-failure path: a single taxonomy failure still propagates unchanged (the upstream testExceptionDuringRefresh passes with both).
B. Use IOUtils.closeWhileSuppressingExceptions(t1, newReader), the module's idiom (validated by the checks) (recommended)
In SearcherTaxonomyManager.refreshIfNeeded (SearcherTaxonomyManager.java:205-212), -5/+1.
} catch (Throwable t1) {
- try {
- IOUtils.close(newReader);
- } catch (Throwable t2) {
- t2.addSuppressed(t2);
- }
+ IOUtils.closeWhileSuppressingExceptions(t1, newReader);
throw t1;
}Trade-offs
- The same code as the sibling
DirectoryTaxonomyReader.java:221-223; it removes the hand-written block that held the typo. - By reading
IOUtils.java:114-137: the close failure is added tot1as suppressed andt1is rethrown; if the close throws anErrorandt1is not one, thatErroris thrown instead, witht1suppressed. - The helper exists from 10.4.0 on (added by #14633; absent at the 10.3.2 and 9.12.3 tags), so a backport to an older branch would use A.
What the checks validated
- Stock
645d4fe14a20: the repro fails with assertions on and off. - With B: the repro and
TestSearcherTaxonomyManagerpass with assertions on and off (7 tests, 0 failures, 1 skipped:testNRTis@Nightly); the whole:lucene:facet:testpasses with assertions on (40 suites, 307 tests, 0 failures, 7 skipped). checkGoogleJavaFormat,ecjLintMain,ecjLintTest,forbiddenApisMainandforbiddenApisTestof:lucene:facetpass once the repro's long comment on line 60 is wrapped onto two lines, which the formatter asks for (comment only; the fixed class was not flagged).
In SearcherTaxonomyManager.java:209, +1/-1.
} catch (Throwable t2) {
- t2.addSuppressed(t2);
+ t1.addSuppressed(t2);
}Trade-offs
- The evident intent and the smallest change; it applies unchanged to every affected branch back to 7.4.
- An
Errorthrown by the close is attached tot1as suppressed instead of being thrown; the hand-written block stays.
What the checks validated
- With A: the repro and
TestSearcherTaxonomyManagerpass with assertions on and off (7 tests, 0 failures, 1 skipped). - Earlier checks ran it on main
ea57360912ed(the finder's, the independent reproducer's and the stock-classes tests, the periodic-refresh probe: all pass) and on 7.4.0 (passes).
A read-only code search found no use of SearcherTaxonomyManager in Solr, Elasticsearch or OpenSearch, so the exposure is mainly applications that use Lucene's facet module directly.
Code reading of the refresh exception paths of Lucene's near-real-time reference managers, while writing a TLA+ model of ReferenceManager and SearcherManager. The model does not cover SearcherTaxonomyManager and no model-checker trace showed this, so it is a code-reading find; a JUnit test then confirmed it.
The method in full142 words
Method: code reading (the study's non-formal tier). A hunting AI tool working on the near-real-time reference managers wrote a TLA+ model of ReferenceManager and SearcherManager with refresh and reader-close failures, and read the refresh exception paths of the subclasses while doing so. It noticed t2.addSuppressed(t2) in SearcherTaxonomyManager.refreshIfNeeded.
- The model covers
ReferenceManagerandSearcherManager, notSearcherTaxonomyManager, and no TLC trace produced this signal; so the find is credited to code reading, not to the model. - The finder confirmed it with a JUnit test that uses two protected extension points (a
DirectoryTaxonomyReadersubclass whosedoOpenIfChangedthrows, and aFilterDirectoryReaderwhose reopened instances fail indoClose). - Independent checks then reproduced it without subclassing any reader: one with plain injected I/O errors on the public API, one with the upstream
testExceptionDuringRefreshscenario plus injected close errors; that scenario became the minimal repro.
No prior report: issues and PRs, the JIRA archive, the dev@, java-user@, issues@, commits@ and builds@ lists and the web (151 queries, 2026-10-26; refreshed 2026-11-01) found only LUCENE-8244 (#9291), the change that introduced the line. No open PR touches the file.
Searches4 searches
Verdict new. The only related item is the introducing change, which reported a different defect (the file-handle leak); its thread never mentions suppression or a double failure. Every "Self-suppression not permitted" CI trace on the lists comes from another call site.
| Where | When | How | Found |
|---|---|---|---|
| GitHub issues and PRs (apache/lucene) | 2026-10-26; refreshed 2026-11-01 | the scripted prior-report search plus 37 GraphQL searches: exact strings (t2.addSuppressed, addSuppressed(t2), "Self-suppression not permitted"), class and method names, LUCENE-8244, recent activity; refresh: SearcherTaxonomyManager (updated since 2026-10-20, and open PRs), Self-suppression, addSuppressed, t1.addSuppressed | only #9291 (LUCENE-8244, introduced the line); #14633 (added IOUtils.closeWhileSuppressingExceptions), #16278 / #16303 (suppressed close failures elsewhere), #13976 (open, SearcherManager and its test): not this bug |
| Open and closed-unmerged PRs by touched file | 2026-10-26; refreshed 2026-11-01 | the file lists of all 413 open and 617 closed-unmerged PRs; refresh: the 58 open PRs updated since 2026-10-26 | no PR changes the line; none of the refreshed PRs touches the file |
| Mailing lists (lists.apache.org) | 2026-10-26 | dev@, java-user@, issues@, commits@, builds@, solr-user@ (18 direct searches plus the scripted search's list queries); 21 CI mails read in full | only the three LUCENE-8244 commit mails; the 11 "Self-suppression not permitted" CI traces come from other call sites |
| JIRA archive and the web | 2026-10-26 | the LUCENE-8244 record and Self-suppression / addSuppressed text searches; 10 web queries; downstream code searches | no report |
Introduced on 2018-04-28 by the LUCENE-8244 leak fix (4fba55c8642; backport 9f2eda7a1fc), first released in 7.4.0. 7.3.1 passes, 7.4.0 fails, and 7.4.0 with the one-token fix passes; every release executed (nine from 7.4.0 to 10.5.2) fails, and 10.6.0 and main have the same line.
Versions tested7 rows
Introduced by 4fba55c8642 "LUCENE-8244: Do not leak open file descriptors in SearcherTaxonomyManager's refresh on exception" (Mike McCandless, 2018-04-28) and its same-day branch_7x backport 9f2eda7a1fc, which 7.4.0 contains. No PR: the patch attached to the JIRA issue (now #9291) already had t2.addSuppressed(t2). git log --all -S 't2.addSuppressed(t2)' finds these two commits and aad44bc6e36 (2021, "Leave a note saying where branch_8x lives."), which deleted every file of the old lucene-solr branch_8x; no commit changes or fixes the line. The line is at every release tag from 7.4.0 to 10.6.0 and at none from 7.0.0 to 7.3.1; the clone has no tags for 8.8.2 to 8.11.4, so 8.8.2 to 8.11.3 were neither run nor read (they lie between 8.8.1, line present, and 8.11.4, fails). Every run used JDK 25; the deciding JDK rule has been specified since Java 7.
| Version or commit | Verdict | Result |
|---|---|---|
| 7.3.1 | not affected | passes: the taxonomy IOException propagates (7.3.1 never closes the new reader; it leaks 4 index files instead, the old LUCENE-8244 bug) |
| 7.4.0 | affected | fails with assertions on and off: "Self-suppression not permitted", taxonomy failure missing |
7.4.0 + t1.addSuppressed(t2) | causality | passes: the taxonomy IOException with the close failure suppressed |
| 7.7.3, 8.0.0, 8.11.4, 9.0.0, 9.12.3, 10.0.0 | affected | fail (release jars) |
| 10.5.2 | affected | fails with assertions on and off |
10.6.0, branch_10_6, branch_11x, upstream main 2f424fd428d | by reading | the same file as 645d4fe14a20 (line 209 unchanged) |
main 645d4fe14a20 | affected | the minimal test fails through gradle with assertions on and off; passes with either fix |
Confirmed with high confidence: the finder, an independent reproducer, a devil's-advocate verifier and an adjudication each reproduced it, two of them without subclassing any reader. A reviewer from another model family, unable to run code, returned a procedural "not real" while agreeing with the root cause.
Checks and verdicts391 words
Everyone agrees on the root cause and that the harm is a wrong exception only.
| Check | Verdict | What it found |
|---|---|---|
| finder | candidate | JUnit with a DirectoryTaxonomyReader subclass and a FilterDirectoryReader fails on main (assertions on and off, 20/20 iterations), 10.5.2 and 9.12.3; passes with t1.addSuppressed(t2). |
| cross-model review, refute lens (OpenAI GPT-5.6, read-only) | not real, low (procedural) | Could not run code; its notes say the verdict "is not a technical refutation". Root cause agrees, inputs in contract, intended argument weak. A separate run re-ran the repro: fails. |
| independent reproducer | real | Own test from the claim alone, public API plus injected I/O errors: fails on main (assertions on and off, 25/25 random seeds), 10.5.2 and 9.12.3; the control passes; no leaked files; the fix passes. |
| cross-model review, intended lens (OpenAI GPT-5.6, read-only) | real, medium | Intended-behaviour argument weak; the fix fixes it. A separate run re-ran the repro on main: fails; with the fix: passes. |
| devil's-advocate verifier | real, low severity | The upstream testExceptionDuringRefresh plus injected close IOExceptions fails on main (assertions on and off, 4 seeds), 10.5.2 and 9.12.3, and passes with the fix; no intended-behaviour argument survived. |
| adjudication | confirmed, high | Re-ran the reproducer's test (fails with assertions on and off; passes with the fix) and the stock-classes test (fails). Wrong error, low severity. |
| history, security screen, audit, re-checks | 7.4.0 first affected; security none; confirmed | 20 history runs (7.3.1 passes, 7.4.0 to 10.5.2 and main fail, 7.4.0 with the one-token fix passes); the periodic-refresh probe (task cancelled after 1 tick; the fix keeps it running); the credibility audit re-runs; the 2026-10-26 current-main re-check; the 2026-10-29 minimization on 645d4fe14a20. |
| this doc (2026-11-01) | reproduced, both fixes validated | Gradle on 645d4fe14a20: the minimal test fails with assertions on and off; fix B passes it, the facet module's 307 tests run with 0 failures (7 skipped), and lint passes; fix A passes it and TestSearcherTaxonomyManager. |
| fact checks of this doc (2026-11-01) | bug real; text corrected | An independent fact check re-ran the minimal test and both fixes (assertions on and off, through gradle and standalone) and counted the injected close failures; a cross-model fact check (OpenAI GPT-5.6) re-read the code, history and logs. Both found the bug real and not security-sensitive; the text errors they found are corrected here. |
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 452 KB
Finding record, Lucene and upstream sources, repro files and diagram notes1,338 words
- Registry title
- SearcherTaxonomyManager refresh calls t2.addSuppressed(t2), so when the taxonomy reopen fails and closing the just-reopened index reader also fails, maybeRefresh throws IllegalArgumentException ('Self-suppression not permitted') and the taxonomy failure is lost
- Area
- facet / taxonomy (SearcherTaxonomyManager.refreshIfNeeded exception path)
- Priority
- Low, rank 1080: SearcherTaxonomyManager throws a self-suppression IllegalArgumentException that hides the taxonomy failure, but only when two I/O failures hit one refresh.
- Impact class
- wrong error
- Impact note
- Applications on Lucene 7.4.0 through 10.6.0 (10.6.0 by reading) or main that use SearcherTaxonomyManager (taxonomy facets with NRT, an opt-in module) are hit only when one refresh suffers two failures at once, the taxonomy reopen fails and closing the just-reopened index reader also fails (in practice two I/O errors), and then maybeRefresh() or maybeRefreshBlocking() throws an unchecked IllegalArgumentException whose cause is the secondary close failure instead of the documented IOException carrying the taxonomy failure, which is missing from the whole exception chain; nothing leaks, no data is wrong and the manager stays usable, but a caller that catches only IOException is bypassed (executed: a periodic ScheduledExecutorService refresh task that catches IOException is silently cancelled after that one refresh, so the searcher stays at its old point in time until the application refreshes again).
- Summary
- You use SearcherTaxonomyManager, and one refresh hits two I/O failures at once: reopening the taxonomy fails, and closing the index reader that was just reopened also fails. You expect maybeRefresh() to throw the taxonomy failure as an IOException, its documented contract, with the close failure attached as a suppressed exception. Instead it throws IllegalArgumentException('Self-suppression not permitted'), because the cleanup code calls t2.addSuppressed(t2) where t1.addSuppressed(t2) was meant, so the taxonomy failure is lost entirely and callers that catch IOException do not see it.
- Introduced
- 4fba55c864298848a07c2971738947473484006e (master), 'LUCENE-8244: Do not leak open file descriptors in SearcherTaxonomyManager's refresh on exception', mikemccand, 2018-04-28; the same-day branch_7x backport 9f2eda7a1fc6d4cec70a8a76f3401a6bd53a48b1 is the commit that 7.4.0 contains. No pull request: the change went in as LUCENE-8244.patch on JIRA LUCENE-8244 (now #9291), and the attached patch already contains t2.addSuppressed(t2) [read: git blame of lines 203-212 and git log --all -S 't2.addSuppressed(t2)' in the full-history clone find only these two commits; exec: 7.3.1 passes, 7.4.0 fails, and 7.4.0 with the one-token fix shadowed passes]. A defect in the LUCENE-8244 fix itself: before it, the same path leaked the reopened index reader (the LUCENE-8244 bug) but did propagate the taxonomy failure (2018-04-28)
- First affected version
- 7.4.0 [exec] (tag 2018-06-18; its CHANGES.txt lists LUCENE-8244 under Bug Fixes); last unaffected release 7.3.1 [exec] (tagged 2018-05-06 from branch_7_3, which never received the backport; it reports the taxonomy failure but leaks the reopened reader, the pre-existing LUCENE-8244 bug)
- Affected versions
- every release from 7.4.0 through 10.5.2, 10.6.0 (by reading), and main ea57360912ed [read: the line is present at every release tag 7.4.0..10.5.2 in the history clone (the 8.9.0-8.11.x tags are not in the clone; 8.11.4 was executed) and absent at every tag 7.0.0..7.3.1, git log --all -S shows no removal on any branch, and branch_10x and branch_10_6 (:209) and branch_9_12 (:174) still have it; exec: a public-API history probe fails on 7.4.0 (-ea and -da), 7.7.3, 8.0.0, 8.11.4, 9.0.0, 9.12.3, 10.0.0, 10.5.2 (-ea and -da) and main (-ea and -da) and passes on 7.3.1; the finder's repro fails on 7.6.0, 8.11.4, 9.12.3, 10.5.2 and main (it does not compile on 7.5.0, whose SearcherTaxonomyManager has no reader-based constructor yet); the independent reproduction's and the intended-behaviour check's repros fail on main (-ea and -da), 10.5.2 and 9.12.3]; not fixed upstream. All runs used JDK 25; the deciding JDK behaviour (Throwable.addSuppressed(this) throws IllegalArgumentException) is specified since Java 7, so the 2018-era outcome follows from that contract but was not executed on Java 8
- 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
- non-formal: code reading
- Upstream and prior reports
- none found (2026-10-26: 151 recorded queries, namely two scripted prior-report searches (55 queries over apache/lucene issues, PRs and commits, downstream repositories, the dev@, java-user@, issues@ and commits@ lists, the LUCENE JIRA archive and the web, then 24 over the lists and JIRA only), 54 GitHub GraphQL searches (37 on apache/lucene, 17 downstream and global) and 18 direct lists.apache.org searches including builds@ and solr-user@; plus the changed files of all 413 open and all 617 closed-unmerged apache/lucene PRs, the JIRA REST record of LUCENE-8244, 21 CI failure mails read in full, and a full local history search; every medium-overlap hit and 17 recorded items read, none the same bug). The only related item is the introducing issue #9291 / LUCENE-8244 (mikemccand, 2018, the file-handle leak): its fix created the line, and its thread never mentions suppression or a double failure; every 'Self-suppression not permitted' CI trace comes from another call site. Not fixed upstream: upstream main was still at ea57360912ed with a byte-identical file at search time, the line is on branch_10x, branch_10_6 and branch_9_12, and no open or closed-unmerged PR changes it
- Lucene commit tested
- main
645d4fe14a20(2026-11-01): the minimal test fails through gradle in a scratch worktree with assertions on and off (IllegalArgumentException: Self-suppression not permittedatSearcherTaxonomyManager.java:209, caused by the injected close failure); with fix B it passes, and the whole facet module passes (40 suites, 307 tests, 0 failures, 7 skipped) with the module's format, ECJ and forbidden-apis checks; with fix A the test andTestSearcherTaxonomyManagerpass. The sameSearcherTaxonomyManager.javais on upstream main2f424fd428d,branch_11x,branch_10_6and 10.6.0 (read). Earlier runs: a version-portable probe on 7.3.1 (pass) and 7.4.0, 7.7.3, 8.0.0, 8.11.4, 9.0.0, 9.12.3, 10.0.0, 10.5.2 and mainea57360912ed(fail), the hunt's and verifiers' tests onea57360912edanda9779ef3c193, and the minimal test, standalone, on645d4fe14a20.
- Finding record
lucene/facet/src/java/org/apache/lucene/facet/taxonomy/SearcherTaxonomyManager.java:188-225 (at ea57360912ed) (refreshIfNeeded: index reopen first, then the taxonomy), :203-212 (try TaxonomyReader.openIfChanged :204; catch (Throwable t1) :205; IOUtils.close(newReader) :207; catch (Throwable t2) :208; t2.addSuppressed(t2) :209; throw t1 :211)lucene/core/src/java/org/apache/lucene/search/ReferenceManager.java:72-74 (at ea57360912ed) (refreshIfNeeded '@throws IOException if the refresh operation failed'), :203 and :231 (maybeRefresh and maybeRefreshBlocking '@throws IOException if refreshing the resource causes an IOException')lucene/facet/src/test/org/apache/lucene/facet/taxonomy/TestSearcherTaxonomyManager.java:343-364 (at ea57360912ed) (testExceptionDuringRefresh, added by LUCENE-8244: deletes the taxonomy segments file :363 and asserts IndexNotFoundException from maybeRefreshBlocking :364; single failure only); lucene/facet/src/java/org/apache/lucene/facet/taxonomy/directory/DirectoryTaxonomyReader.java:222 (IOUtils.closeWhileSuppressingExceptions(t, r2), the sibling idiom); lucene/CHANGES.txt:7443-7444 (LUCENE-8244 entry in the Lucene 7.4.0 section, heading :7306), :7160-7161 (LUCENE-8459: the reader-based constructor allows a FilterDirectoryReader); lucene/core/src/java/org/apache/lucene/search/ControlledRealTimeReopenThread.java:214-215 (a refresh IOException is wrapped in RuntimeException, ending the thread)
- The finding in 30 seconds
a Lucene main checkout (645d4fe14a20) lucene/facet/src/java/org/apache/lucene/facet/taxonomy/SearcherTaxonomyManager.java:187-225 (195, 203-212; 209 the self-suppression)a Lucene main checkout (645d4fe14a20) lucene/core/src/java/org/apache/lucene/search/ReferenceManager.java:154-188 (167 refreshIfNeeded call; 180-187 finally blocks)JDK 25 lib/src.zip java.base/java/lang/Throwable.java:246, 1094-1101 (addSuppressed self-suppression check)a Lucene main checkout (645d4fe14a20) git grep -nP '\b(\w+)\.addSuppressed\(\1\)' -- '*.java': one hit, lucene/facet/src/java/org/apache/lucene/facet/taxonomy/SearcherTaxonomyManager.java:209
- Who is affected
a Lucene main checkout (645d4fe14a20) lucene/core/src/java/org/apache/lucene/search/ControlledRealTimeReopenThread.java:212-216
- Minimal repro
a Lucene main checkout (645d4fe14a20) lucene/facet/src/test/org/apache/lucene/facet/taxonomy/TestSearcherTaxonomyManager.java:343-366
- Why it looks unintended
a Lucene main checkout (645d4fe14a20) lucene/facet/src/java/org/apache/lucene/facet/taxonomy/SearcherTaxonomyManager.java:203-212a Lucene main checkout (645d4fe14a20) lucene/core/src/java/org/apache/lucene/search/ReferenceManager.java:72, 203, 231a Lucene main checkout (645d4fe14a20) lucene/facet/src/test/org/apache/lucene/facet/taxonomy/TestSearcherTaxonomyManager.java:343-366 (testExceptionDuringRefresh; 361-364 delete segments file, expect IndexNotFoundException)a Lucene main checkout (645d4fe14a20) lucene/facet/src/java/org/apache/lucene/facet/taxonomy/directory/DirectoryTaxonomyReader.java:221-223 (closeWhileSuppressingExceptions idiom)a Lucene main checkout (645d4fe14a20) lucene/CHANGES.txt:7476-7477 (LUCENE-8244, 7.4.0 Bug Fixes; section heading :7339, Bug Fixes :7454)a Lucene main checkout (645d4fe14a20) @lucene.experimental: lucene/core/src/java/org/apache/lucene/search/ReferenceManager.java:35, lucene/facet/src/java/org/apache/lucene/facet/taxonomy/TaxonomyReader.java:57, lucene/facet/src/java/org/apache/lucene/facet/taxonomy/directory/DirectoryTaxonomyReader.java:61; lucene/facet/src/java/org/apache/lucene/facet/taxonomy/SearcherTaxonomyManager.java:34-41 (no tag)SECURITY.md:30-32 (index files are trusted)
- Suggested fix
a Lucene main checkout (645d4fe14a20) lucene/core/src/java/org/apache/lucene/util/IOUtils.java:97-137; lucene/facet/src/java/org/apache/lucene/facet/taxonomy/directory/DirectoryTaxonomyReader.java:221-223
- History
a Lucene clone (fetched 2026-10-31): blob 64f906a8f03 of lucene/facet/src/java/org/apache/lucene/facet/taxonomy/SearcherTaxonomyManager.java at releases/lucene/10.6.0, origin/branch_10_6, origin/branch_11x, origin/main 2f424fd428d
- TestSearcherTaxonomyManagerDoubleFailure.java:
repro/LFV-264/src/org/apache/lucene/facet/taxonomy/TestSearcherTaxonomyManagerDoubleFailure.java(66 lines)
Made in parallel with this doc from the code at 645d4fe14a20 and the repro's input and output.
assets/LFV-264/bug.png(452 KB)
