A commit makes file names durable with one directory fsync, then publishes itself by renaming pending_segments_N to segments_N. NRTCachingDirectory writes its cached segment files to disk between the two, so their names are not covered.
- Bytes and names. A file fsync makes a file's bytes durable. A directory fsync (
syncMetaData) makes its names durable: which files exist, and renames. Since LUCENE-8048, Lucene assumes that names in one directory may reach the disk in any order. - Commit.
commit()fsyncs the directory, writespending_segments_N, fsyncs every file the commit references, then renamespending_segments_Ntosegments_N. The rename publishes the commit, so a crash should leave the old commit or the new one. - NRTCachingDirectory. It keeps small newly flushed or merged segment files in RAM and writes them to the wrapped directory when the commit calls
sync.
_1 files created after it, the rename, the crash), the crash state with its exception, and the fix (NRTCachingDirectory.sync, plus unCache returning whether it created a file). Select the still to open it at full size. Generated with AI, checked against the code.- Index through
new NRTCachingDirectory(dir, 5.0, 60.0)(the class javadoc's example) and commit twice, one document each. The second commit's new segment_1is tiny, so its 3 files (_1.cfe,_1.cfs,_1.si) sit in the RAM cache. - Commit 2 first fsyncs the index directory, so the 4 names on disk (
_0.cfe,_0.cfs,_0.si,segments_1) are durable. Only then doesNRTCachingDirectory.synccopy the 3_1files to disk; with anFSDirectorydelegate only their bytes are fsynced, and it makes nosyncMetaData()call. - Commit 2 then renames
pending_segments_2tosegments_2and fsyncs the directory again. You expect a crash at any moment to leave commit 1 or commit 2. - Instead, if the machine crashes when the rename has reached the disk but the 3
_1names have not (an order the crash model Lucene adopted in LUCENE-8048 allows),segments_2points at a missing_1.si:DirectoryReader.open,listCommitsandnew IndexWriterall throwCorruptSegmentInfoException. - A plain
FSDirectoryopens fine under the same crash model: its_1files already exist at the first fsync. Deletingsegments_2by hand recovers commit 1.
Applications that index through NRTCachingDirectory (opt-in, @lucene.experimental; Solr's default configset uses its factory) and suffer an OS crash or power loss inside commit(), on a file system that persists the rename before earlier creates. A commit with one cached segment suffices. Rare: ext4 data=ordered and XFS are not expected to reorder like this, and a JVM crash cannot cause it.
Mechanism, terms and numbers513 words
NRTCachingDirectory keeps small newly flushed segment files in RAM and writes them to the wrapped directory when IndexWriter.commit() calls Directory.sync. Since LUCENE-8048 (7.2), a commit fsyncs the index directory in SegmentInfos.prepareCommit so that every name it references is durable before pending_segments_N is renamed to segments_N, the step that publishes the commit. IndexWriter.startCommit calls directory.sync only after that fsync, so with NRTCachingDirectory the new segment files are created on disk after the barrier, and the next directory fsync comes only after the rename. On a file system that persists that rename before the earlier creates (the model LUCENE-8048 adopted; ext4 data=ordered and XFS are not expected to do it), an OS crash or power loss in that window leaves a newest segments_N whose files are missing: DirectoryReader.open, listCommits and new IndexWriter all throw until someone deletes it by hand, which recovers the previous commit. A plain FSDirectory passes the same crash test: its files exist before the barrier. One deterministic test shows it on main, 10.6.0 and older releases; all 71 releases from 7.2.0 to 10.5.2 fail the hunt's longer tests. One directory fsync in NRTCachingDirectory.sync when it wrote cached files fixes it.
- The barrier.
SegmentInfos.prepareCommit(SegmentInfos.java:966-972) callsdir.syncMetaData()(:970, added by LUCENE-8048) and thenwrite(dir), which writes and fsyncspending_segments_N(:620). Every name that exists at:970is durable from then on. - The order.
IndexWriter.startCommitcallstoSync.prepareCommit(directory)(IndexWriter.java:5974) and only afterwardsfilesToSync = toSync.files(false); directory.sync(filesToSync);(:5992-5993). - What is cached.
NRTCachingDirectory.createOutputsends a file to the RAM cache whendoCacheWriteaccepts it (NRTCachingDirectory.java:159-163): a flush or merge whose estimated size is at mostmaxMergeSizeMBwhile the cache stays undermaxCachedMB(:231-245). So a small new segment is not in the wrapped directory at step 1. - Where the files are created.
NRTCachingDirectory.sync(:169-178) callsunCachefor each name;unCachecreates the file in the wrapped directory within.copyFrom(:305-323, the copy at:319). Thenin.sync(fileNames)(:177) fsyncs the files; with anFSDirectorydelegate, their bytes only (FSDirectory.sync,FSDirectory.java:244-251). The class itself never callssyncMetaData. - The publish.
SegmentInfos.finishCommit(:996-1012) renamespending_segments_Ntosegments_N(:1005) and only then fsyncs the directory (:1007). - Result. Between
:1005and the end of:1007, the rename and the creates of step 4 are both pending, with no order between them. If the rename reaches the disk first and the machine crashes, the newestsegments_Nnames files that are not there (in the test:_1.si,_1.cfs,_1.cfe). With a plain directory those files already exist at step 1, so the plain control passes.
MockDirectoryWrapper.crash() damages or deletes only files whose bytes were never fsynced and never undoes creates, renames or deletes that lacked a directory fsync: "TODO: we should also mess with any recent file renames, file deletions, if syncMetaData was not called!!" (MockDirectoryWrapper.java:339-340, :526). TestNRTCachingDirectory has no crash test.
Exposure and likelihood270 words
- Who. Applications that write through
NRTCachingDirectory, the near-real-time setup its javadoc shows (new NRTCachingDirectory(fsDir, 5.0, 60.0),NRTCachingDirectory.java:50-58). Opt-in and@lucene.experimental(:63); not the defaultDirectory. Per the prior-report search notes, Solr's_defaultconfigset usesNRTCachingDirectoryFactory; whether Solr's commit path reaches this was not checked. - Trigger. A machine-level failure (OS crash, kernel panic, power loss) inside
commit(), between thesegments_Nrename and the directory fsync after it, on a file system that persists that rename before the earlier creates. POSIX and Linuxfsync(2)do not promise that order; it is the model LUCENE-8048 accepted for plain directories. Mainstream ordered-metadata journaling (ext4data=ordered, XFS) is not expected to reorder like this, and no physical file system was tested. A JVM crash orkill -9cannot produce the state: completed creates and renames stay visible to the next process whether or not they were fsynced. - Effect. Index unusable. In an independent reproducer's probe on the crash state,
DirectoryReader.open,DirectoryReader.listCommitsandnew IndexWriter(CREATE_OR_APPEND)all throw; deleting the newestsegments_Nby hand recovers the previous commit (after deletingsegments_2the index opens withmaxDoc=10). Whoever does that should back up the index first and make sure no reader or writer has it open. Fail-stop: in every enumerated crash state this bug causes, opening throws; none returned wrong or deleted documents. One commit that contains a cached segment is enough; whether a segment is cached depends on the flush or merge context, its estimated size and the cache's free space (NRTCachingDirectory.java:231-245). - How likely. Rare; no field report attributable to it was found.
One JUnit test (public API plus FilterDirectory's protected constructor and in field), RAM directories only, deterministic. A FilterDirectory under the writer remembers which names existed at the last directory fsync; at the rename to segments_2 it copies just those files plus segments_2 into a second directory, the crash state (all bytes kept, which is generous to Lucene), and opens it.
/*
* 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.store;
import java.io.IOException;
import org.apache.lucene.document.Document;
import org.apache.lucene.index.DirectoryReader;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.tests.util.LuceneTestCase;
public class TestNRTCachingDirectoryCommitCrash extends LuceneTestCase {
public void testCrashRightAfterSecondCommitRename() throws Exception {
Directory crashState = new ByteBuffersDirectory();
Directory delegate =
new FilterDirectory(new ByteBuffersDirectory()) {
String[] durableNames = new String[0];
@Override
public void syncMetaData() throws IOException {
super.syncMetaData();
durableNames = listAll();
}
@Override
public void rename(String source, String dest) throws IOException {
super.rename(source, dest);
if (dest.equals("segments_2")) {
for (String name : durableNames) {
if (name.equals(source) == false) {
crashState.copyFrom(in, name, name, IOContext.DEFAULT);
}
}
crashState.copyFrom(in, dest, dest, IOContext.DEFAULT);
}
}
};
try (IndexWriter w =
new IndexWriter(new NRTCachingDirectory(delegate, 5.0, 60.0), new IndexWriterConfig())) {
w.addDocument(new Document());
w.commit();
w.addDocument(new Document());
w.commit();
}
// expected: crash state opens at commit 1 or 2; actual: _1 files were created after the fsync
try {
DirectoryReader.open(crashState).close();
} catch (IOException e) {
fail(
"crash state (names durable at the last directory fsync, plus the segments_2 rename)"
+ " should open at commit 1 or 2, but: "
+ e);
}
}
}
cd $LUCENE && ./gradlew :lucene:core:test --tests TestNRTCachingDirectoryCommitCrash -Ptests.seed=DEADBEEF
- Expected
- The crash state opens at commit 1 or commit 2.
- Observed
DirectoryReader.openthrowsCorruptSegmentInfoException: segment info file: _1.si cannot be read; the crash state has no_1.*file.
More files, environment, full output356 words
The crash state is the one LUCENE-8048's model allows: the rename reached the disk, and no name created since the last directory fsync did. Every file's bytes are kept, which is generous to Lucene. Skipping the rename source while copying is part of the model: with either fix pending_segments_2 is durable before the rename, on stock code it never is.
It is a crash-model test, not a physical crash: no file system was crashed. It covers only the second commit, because a crash during the very first commit may leave a corrupt index by design (LUCENE-4738). The seed only drives the test framework's randomisation; the test also fails with seeds 1234ABCD and C0FFEE01. It passes checkGoogleJavaFormat, ecjLintTest and forbiddenApisTest as is.
Regression test: this same test; it passes with either fix.
| lucene | main 645d4fe14a20 (11.0.0-SNAPSHOT): fails; with either fix: passes. Also fails on the 10.6.0, 10.5.2, 9.12.3 and 9.1.0 release jars |
|---|---|
| jdk | OpenJDK 25.0.3 (build 25.0.3+9-LTS) |
| os | Linux, aarch64 (the only platform run; pure Java, RAM directories only) |
cp $REPRO/TestNRTCachingDirectoryCommitCrash.java $LUCENE/lucene/core/src/test/org/apache/lucene/store/
cd $LUCENE && ./gradlew :lucene:core:test --tests TestNRTCachingDirectoryCommitCrash -Ptests.seed=DEADBEEF
# assertions off: add -Ptests.asserts=false (same failure)
# gradle, scratch worktree of 645d4fe14a20, -Ptests.seed=DEADBEEF (assertions on; the same with -Ptests.asserts=false)
java.lang.AssertionError: crash state (names durable at the last directory fsync, plus the segments_2 rename)
should open at commit 1 or 2, but: org.apache.lucene.index.CorruptSegmentInfoException: segment info file:
_1.si cannot be read - it may be missing or corrupt (resource=BufferedChecksumIndexInput((clone of)
ByteBuffersIndexInput (file=segments_2, buffers=246 bytes, block size: 1, blocks: 1, position: 0)))
at org.apache.lucene.store.TestNRTCachingDirectoryCommitCrash.testCrashRightAfterSecondCommitRename(TestNRTCachingDirectoryCommitCrash.java:64)
:lucene:core:test (FAILURE): 1 test, 1 failure
# released jars, standalone runner, seed DEADBEEF
10.6.0 (-ea, -da) ... but: org.apache.lucene.index.CorruptSegmentInfoException: segment info file: _1.si cannot be read ...
10.5.2, 9.12.3, 9.1.0 (-da) ... but: org.apache.lucene.index.CorruptIndexException: Unexpected file read error while reading index. ...
# crash-state files (stock main): [_0.cfe, _0.cfs, _0.si, segments_1, segments_2]
# independent impact probe on that state: DirectoryReader.open, DirectoryReader.listCommits and
# new IndexWriter(CREATE_OR_APPEND) throw CorruptSegmentInfoException;
# after deleting segments_2 by hand: opens, maxDoc=10 (commit 1 of that probe's 10-documents-per-commit test)
# with fix A or fix B: 1 test, 0 failures (assertions on and off)- Its javadoc says "This is safe to use": on commit, "all cached files will be flushed from the cached and sync'd".
commit()promises to "survive an OS or machine crash or power loss"; its protocol is built to leave the old or new commit.- LUCENE-8048 added the directory fsync for exactly this failure; it runs before
NRTCachingDirectorywrites its files.
The full argument262 words
- Contracts.
NRTCachingDirectory: "This is safe to use: when your app calls {IndexWriter#commit}, all cached files will be flushed from the cached and sync'd" (NRTCachingDirectory.java:47-48).IndexWriter.commit: commits "and syncs all referenced index files, such that a reader will see the changes and the index updates will survive an OS or machine crash or power loss", andDirectory#sync"should not return until the file contents and metadata are on stable storage" (IndexWriter.java:4188-4201).Directory.sync: "Lucene uses this to properly commit changes to the index, to prevent a machine/OS crash from corrupting the index" (Directory.java:104-112). - The design. The commit is published by one rename (
Directory.java:131: rename "is used by IndexWriter to publish commits"). LUCENE-8048 (#9096, CHANGES 7.2.0 "Bug Fixes") addeddir.syncMetaData()toprepareCommitfor this failure on plain directories; its reporter's fix was "sync index directory after all segments have been written", and withNRTCachingDirectorythe segments are written to disk only insidesync(), after that barrier. - History (read, not executed). In 4.8-4.10
FSDirectory.syncitself fsynced the directory after the files (LUCENE-5588), which covered the filesNRTCachingDirectoryhad just created; 5.0 (LUCENE-5925) moved that fsync after the rename for every directory; 7.2 (LUCENE-8048) restored a barrier only before the point whereNRTCachingDirectorycreates its files. No javadoc, CHANGES or MIGRATE entry, test or discussion exemptsNRTCachingDirectory. - Tests. No upstream test models lost directory updates (the
MockDirectoryWrapperTODO above). Scope: only the second-commit case is claimed, because a crash during the very first commit may leave a corrupt index by design (LUCENE-4738,TestIndexWriterOnJRECrash.java:164-170).
The devil's-advocate check244 words
The strongest case: Directory.sync promises only file bytes ("any writes to these files are moved to stable storage", Directory.java:105) and names are syncMetaData's job; the crash falls inside an in-flight commit, before commit() returns; the class is @lucene.experimental; and a committer on LUCENE-8048 (Robert Muir) said a file system that behaves this way "is choosing to corrupt users data" and agreed to the barrier "just to be safe" (#9096).
Why it fails: IndexWriter.commit's javadoc asks Directory#sync for "file contents and metadata" (not decisive alone: sync and syncMetaData are separate APIs, Directory.java:104-119), and either way the combination breaks commit's documented promise to survive an OS crash or power loss, and the class's "safe to use"; no javadoc says which commit an interrupted commit leaves, but the pending_segments_N + rename protocol and LUCENE-8048 are built so that it leaves the old one or the new one, and here the index opens to neither, which is what LUCENE-8048 set out to prevent; @lucene.experimental marks API stability, not a durability exemption; and committers still made the barrier part of the protocol. Reachability lowers impact; it does not make the gap intended. Verdict: not intended, a defect. Cross-model reviewers (OpenAI GPT-5.6) rated the case for "intended" moderate and returned "real"; an intended-behaviour devil's advocate found that none of its seven points survives and returned "real". No physical file system was crashed, so the real-world reach may be close to zero on ordered-metadata file systems.
Recommended Fsync the directory in NRTCachingDirectory.sync when it wrote cached files (validated)
In NRTCachingDirectory.sync, call in.syncMetaData() when it wrote cached files to disk, so their names are durable before the rename (+10/-3 lines). The test passes, as do the closest store and commit suites. The alternative, one more directory fsync in SegmentInfos.finishCommit before the rename, also passes; it costs every Directory one more fsync per commit.
boolean created = false;
for (String fileName : fileNames) {
created |= unCache(fileName);
}
in.sync(fileNames);
if (created) {
// files created in the delegate here must have durable directory entries before the
// commit's segments_N rename (the LUCENE-8048 barrier ran before they existed)
in.syncMetaData();
}All options and trade-offs2 options ยท 707 words
Both fix sites were built and run on a scratch worktree of 645d4fe14a20. A keeps the extra fsync to the affected class; B also covers any other Directory whose sync creates files. Offer both; maintainers may prefer either. Neither was benchmarked.
Fsync the directory in NRTCachingDirectory.sync when it wrote cached files (validated) (recommended)
unCache returns true when it copied a cached file into the wrapped directory (false when the file was not cached), and sync calls in.syncMetaData() after in.sync(fileNames) when it did. Same change as the hunt's and the independent reproducer's fix.
boolean created = false;
for (String fileName : fileNames) {
created |= unCache(fileName);
}
in.sync(fileNames);
if (created) {
// files created in the delegate here must have durable directory entries before the
// commit's segments_N rename (the LUCENE-8048 barrier ran before they existed)
in.syncMetaData();
}Trade-offs
- One extra directory fsync per commit, only when the commit had cached files to write, in a class meant to cut small-file I/O. It runs inside
startCommit, the first phase of the two-phase commit, which theprepareCommitjavadoc says "syncs the index files" (IndexWriter.java:3721-3725). - Fixes only
NRTCachingDirectory; another wrapper whosesynccreates files would need the same. - Variant: call
in.syncMetaData()unconditionally (simpler, but a directory fsync on everysynccall, includingSegmentInfos.write's sync ofpending_segments_N,SegmentInfos.java:620: two per commit).
What the checks validated
- The v2 re-run (own scratch worktree of
645d4fe14a20, removed afterwards; gradle, JDK 25, seed DEADBEEF): the minimal test fails on the stock worktree (1 test, 1 failure, the documented CorruptSegmentInfoException on_1.si); with the fix, the minimal test,TestNRTCachingDirectory,TestIndexWriterCommit,TestCrash,TestCrashCausesCorruptIndexandTestSegmentInfospass (6 suites, 89 tests, 4 skipped, 0 failures); assertions off, the minimal test andTestNRTCachingDirectorypass (61 tests, 2 skipped, 0 failures). The v2 fact check repeated that last pair with assertions on in its own worktree (61 tests, 2 skipped, 0 failures). - Earlier the same day, in another scratch worktree:
:lucene:core:testfororg.apache.lucene.store.*,TestIndexWriterCommit,TestCrash,TestCrashCausesCorruptIndex,TestSegmentInfosandTestIndexFileDeleter, assertions on: 34 suites, 816 tests, 19 skipped, 0 failures (repeated by the independent fact check); assertions off: the minimal test andTestNRTCachingDirectory(61 tests) pass;checkGoogleJavaFormat,ecjLintMain,ecjLintTest,forbiddenApisMainandforbiddenApisTestpass. - Earlier still: the hunt's, the independent reproducer's and the refute reviewer's tests pass with it, and in the bounded crash enumeration the 31 reader-open failures of the
nrtcachingworkload disappear. - Not run: the full Lucene suite, benchmarks, concurrent use (validated single-threaded only). A thread whose
unCachefinds the file already moved ("Another thread beat us",NRTCachingDirectory.java:313) returnsfalse, skips the directory fsync and may return before the other thread's fsync.IndexWriter's own commits do not race this way (startCommit,IndexWriter.java:3915, runs insidesynchronized (commitLock),:3775); the unconditional variant avoids the question.
Call dir.syncMetaData() just before dir.rename(src, dest) in finishCommit (+4 lines with the comment). If it throws, the existing catch rolls the commit back (SegmentInfos.java:1014-1017).
// Directory#sync may create files (NRTCachingDirectory writes its cached files to the
// delegate there), after the directory sync in prepareCommit: make every name this commit
// references durable before the rename publishes it
dir.syncMetaData();
dir.rename(src, dest);Trade-offs
- Covers any
Directorywhosesynccreates files, not onlyNRTCachingDirectory. - One extra directory fsync per commit for every
Directory, including plainFSDirectory, in the second phase of the two-phase commit. - Variant (not run): move the
prepareCommitbarrier here instead of adding one; that keeps today's fsync count but takes that fsync out of the first phase.
What the checks validated
- Earlier on 2026-10-30, scratch worktree of
645d4fe14a20(fix A reverted), gradle, JDK 25, seed DEADBEEF: the minimal test passes, assertions on and off. - The same 34 store and commit suites as for fix A, assertions on: 816 tests, 19 skipped, 0 failures, including
TestNRTCachingDirectory(60 tests, 2 skipped);checkGoogleJavaFormatpasses. - Assertions off: the minimal test and
TestNRTCachingDirectorypass (61 tests);ecjLintMainandforbiddenApisMainpass. The independent fact check repeated the minimal test andTestNRTCachingDirectorywith assertions on and off. - Not run: the full Lucene suite, benchmarks, the hunt's crash enumeration.
Solr's _default configset uses NRTCachingDirectoryFactory; whether Solr's own commit path reaches this gap was not checked.
A crash-state enumerator replayed real IndexWriter commits over a recording Directory and rebuilt the crash states that LUCENE-8048's out-of-order directory-update model allows (a bounded cover). Through NRTCachingDirectory, 31 of 6,340 states could not be opened (75 more belong to a separate issue), none under the ordered model; a TLA+ model and JUnit tests confirmed it.
The method in full199 words
Method: bounded crash-state enumeration of the real jar (the hunt classes it as semi-formal: the first signal came from the executable enumerator, not from a formal tool).
- Enumerator (2026-10-26). A recording
DirectoryunderIndexWriterlogs every create, rename, delete, file fsync and directory fsync. For every crash point it builds each crash state allowed by a bounded cover of the directory operations pending since the last directory fsync, and checks that the index opens to an allowed commit with the right content, that CheckIndex is clean and thatIndexWriterrecovers. Thenrtcachingworkload was added after readingNRTCachingDirectory.syncwhile modelling whichDirectorywrappers create files late. Under the unordered model 106 of 6,340 states failed (0 of 2,350 under the ordered model); this bug's fix alone removes the 31 reader-open failures, and the remaining 75 belong to a separateIndexFileDeleterdurability issue, tracked on its own. - TLA+ model.
CommitDurability.tla(TLC):ReaderOKis violated forNRTCachingDirectoryunder unordered persistence and holds under ordered persistence and with the fix. - Executed tests. The finder's JUnit, an independent reproducer's JUnit and a cross-model reviewer's JUnit fail on stock main and pass with the fix; the minimal repro above is the smallest of them.
None found: no issue, PR or list thread reports files created inside sync() after the commit's directory fsync. LUCENE-8048 (#9096) is the parent: the same failure for plain directories, fixed in 7.2; its thread never mentions NRTCachingDirectory. Searched 2026-10-26, refreshed 2026-10-30.
Searches4 searches
- Parent issue: LUCENE-8048 / #9096 (closed, fixed in 7.2), the same failure mode and crash model for plain directories. Its 24 comments never mention
NRTCachingDirectoryor wrappers that create files insidesync()(one names the test framework'sMockDirectoryWrapperin passing); the reporter's "I'm fairly certain that I didn't find all the places this may be potentially happening" names no site. A maintainer may call this an incomplete fix of LUCENE-8048; that does not make it previously reported. - Symptom only: open #7820 / LUCENE-6762 and its duplicate #7009 (a
segments_Nthat references a missing.si, on Windows with plain MMapDirectory, cause undiagnosed; about CheckIndex recovery), PRs #12872 and #16474 (CheckIndex diagnostics), merged #16476 (names the unreadable.si), #10906 (disk full). - Other NRTCachingDirectory defects, different: #8243 (LUCENE-7188), #9197 (LUCENE-8149), #5309 (LUCENE-4238).
- Limits: keyword search; GitHub full-text search can miss identifiers inside code blocks; one query hit the shared rate limit (covered through the JIRA archive). Tracking issue #16937 does not list this bug.
| Where | When | How | Found |
|---|---|---|---|
| GitHub issues and PRs (apache/lucene) | 2026-10-26 | 16 tool queries plus hand searches, incl. open PRs naming NRTCachingDirectory, syncMetaData, finishCommit, startCommit or SegmentInfos; full reads of #9096 and 9 related threads | no report; #9096 is the parent issue |
| Mailing lists (dev@, java-user@, issues@, commits@), JIRA archive and JIRA REST (LUCENE, SOLR) | 2026-10-26 | 20 list and 4 JIRA-archive queries | no report |
| Solr, Elasticsearch, OpenSearch, Lucene.NET; web | 2026-10-26 | 2 downstream code queries, hand searches, 6 web queries | no report of this mechanism |
| GitHub refresh | 2026-10-30 | read-only GitHub CLI queries: issues and PRs updated since 2026-10-25 for NRTCachingDirectory, syncMetaData, finishCommit, pending_segments, "directory fsync commit"; open PRs naming NRTCachingDirectory; upstream main file history | nothing new; not fixed |
An NRTCachingDirectory-only gap since 7.2.0: LUCENE-8048 (2017) fixed this failure for plain directories with a directory fsync that runs before NRTCachingDirectory.sync creates its cached files, which it has done since 2011. All 71 releases from 7.2.0 to 10.5.2 fail, as do 10.6.0 and main; 6.2.0 to 7.1.0 fail for every Directory (LUCENE-8048 itself).
Versions tested6 rows
Origin: an NRTCachingDirectory-only gap since 7.2.0; the ordering behind it is older. LUCENE-8048 "Filesystems do not guarantee order of directories updates" (#9096; JIRA patch, no PR) added dir.syncMetaData() at the start of prepareCommit: 2bfe7ed0f2f9 on branch_7x (first shipped in 7.2.0; release tag dated 2017-12-19) and 4a5900728db6 on master (merged by 0688be6c6677), both 2017-12-05. That barrier runs before directory.sync in startCommit (both lines from b83dde502bc8, 2012), so it fixed plain directories but not NRTCachingDirectory, whose sync has created the cached files since the class was added (LUCENE-3092: 3525ba940fa2 on trunk, 8035a5ecf0f on branch_3x, 2011-05-17; first released in 3.2) and has never called syncMetaData. So 7.2.0 did not introduce the failure: it is the part of LUCENE-8048's failure that its fix did not reach.
Earlier gap for every directory: 4.8-4.10 fsynced the directory inside FSDirectory.sync (LUCENE-5588), which covered the files NRTCachingDirectory had just created; 5.0 (LUCENE-5925) moved that fsync after the rename, so from 5.0 the window was open for every directory; 7.2 restored a barrier only before the point where NRTCachingDirectory creates its files (read from git, not executed).
Not fixed on upstream main 716d6f216f52, branch_11x 603af8703753, branch_10_6 7e5aec4370e0 or branch_9_12 (read-only checks 2026-10-26 and 2026-10-30; branch_10x was retired on 2026-10-29, #16911).
| Version or commit | Verdict | Result |
|---|---|---|
main 645d4fe14a20 | affected | minimal test fails through gradle (assertions on and off; the assertions-on run repeated in the v2 re-run, both again in the v2 fact check) and on the standalone runner with 3 seeds; passes with fix A and with fix B |
| 10.6.0 (released 2026-10-29) | affected | minimal test fails, -ea and -da (CorruptSegmentInfoException on _1.si) |
| 10.5.2, 9.12.3, 9.1.0 | affected | minimal test fails (CorruptIndexException, cause the missing _1.si) |
| 7.2.0 to 10.5.2 (all 71 releases) | affected | the hunt's and an independent reproducer's longer tests fail on the NRTCachingDirectory second commit; plain-directory and never-caching controls pass |
| 6.2.0 to 7.1.0 (18 releases) | LUCENE-8048 itself | fail too, but so does the plain-directory control: no directory fsync before the rename at all |
| 6.1.0 and older | no verdict | the Directory.rename / syncMetaData hooks the test needs came in 6.2.0 (LUCENE-7373) |
An independent reproducer, a devil's-advocate verifier, an adjudication and a cross-model reviewer (OpenAI GPT-5.6) each ran their own test or re-ran one on stock main and confirmed it; plain-directory controls passed, and the fix passed in every check that ran it. A second GPT-5.6 review found no documented exemption.
Checks and verdicts590 words
Each executed check ran its own test or re-ran an earlier one on stock main (ea57360912ed for the finder, the reviews, the independent reproducer, the works-as-designed and intended-behaviour checks and the adjudication; NRTCachingDirectory, SegmentInfos, IndexWriter, Directory, FilterDirectory and FSDirectory are unchanged from there to 645d4fe14a20; this doc's runs used 645d4fe14a20); the controls (a plain directory, a never-caching NRTCachingDirectory, the fix) pass.
| Check | Verdict | What it found |
|---|---|---|
| Finder (crash-state enumeration) | candidate | nrtcaching workload: 106 of 6,340 unordered-model states fail (a bounded cover; 31 of them this bug's reader-open failures, at the segments_1, segments_2 and segments_3 renames, the first-commit ones not claimed here; the other 75 a separate IndexFileDeleter issue), 0 under the ordered model; the JUnit fails 2 of 3 on main, 10.5.2, 9.12.3 and 8.11.4 and passes with the fix. |
| TLA+ model (TLC) | proof-only | ReaderOK violated for NRTCachingDirectory under unordered persistence; holds under ordered persistence and with the fix. |
| Cross-model review, refute lens (OpenAI GPT-5.6) | real (medium) | Own 2-test JUnit fails 1 of 2 with -ea, -da and a second seed; plain control passes; with the fix its test and TestNRTCachingDirectory pass. Its test crashes the first commit, which this doc does not claim: it confirms the mechanism, not the second-commit case. |
| Independent reproducer | real | Own JUnit fails 2 of 4 on main, 10.5.2, 9.12.3 and 8.11.4 (40 of 80 over 20 iterations); both controls pass; fix 4 of 4; the impact probe shows open, listCommits and IndexWriter all throw. |
| Works-as-designed lens (OpenAI GPT-5.6, reading only) | real (medium) | No documented exemption; its scope correction (claim the second commit only) was accepted. |
| Intended-behaviour devil's advocate | real | A seven-point case for intended does not survive; re-ran: 2 of 3 fail on main and 10.5.2. |
| Adjudication | confirmed (high on mechanism) | Re-ran the independent reproduction's suite (2 of 4 fail, -ea and -da), the finder's (2 of 3) and the independent reproduction's fix (0 of 4). |
| Minimal repro | verified (2026-10-29) | One test fails on main with -ea and -da and 3 seeds; passes with the fix. |
| Brief v1 runs and fact checks | still fails (2026-10-30) | Gradle on scratch worktrees of 645d4fe14a20: stock fails; with fix A or fix B the test and 816 store and commit tests pass; the minimal test fails on the 10.6.0, 10.5.2, 9.12.3 and 9.1.0 jars. Fact checks (one cross-model) found wording and provenance errors, corrected in v2 except the "public API" label and a pre-7.2 parenthetical, which the v2 fact checks caught. |
| Brief v2 re-run | still fails; fix A passes (2026-10-30) | Own scratch worktree of 645d4fe14a20, gradle, seed DEADBEEF: stock 1 test, 1 failure, the documented CorruptSegmentInfoException on _1.si; with fix A the minimal test, TestNRTCachingDirectory, TestIndexWriterCommit, TestCrash, TestCrashCausesCorruptIndex and TestSegmentInfos pass (6 suites, 89 tests, 4 skipped, 0 failures); assertions off the minimal test and TestNRTCachingDirectory pass (61 tests, 2 skipped, 0 failures). |
| Brief v2 fact checks | still fails; both fixes pass (2026-10-30) | Independent fact check on a fresh scratch worktree of 645d4fe14a20: the doc's exact gradle command fails with assertions on and off, output as documented; with fix A the minimal test and TestNRTCachingDirectory pass (61 tests, 2 skipped, 0 failures), with fix B the minimal test passes; its own probe over a real FSDirectory delegate confirms each step and both controls. A cross-model fact check (OpenAI GPT-5.6, reading only) found the doc mostly sound. Their wording errors and overclaims 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 663 KB
Finding record, Lucene and upstream sources, repro files and diagram notes1,607 words
- Registry title
- NRTCachingDirectory.sync creates a commit's cached segment files in the underlying directory after the commit's directory fsync, so an OS crash right after the segments_N rename can leave a newest commit whose files are missing and an index that will not open
- Area
- store / NRTCachingDirectory (commit durability: IndexWriter.startCommit, SegmentInfos.prepareCommit/finishCommit)
- Priority
- Low, rank 605: NRTCachingDirectory (Solr's default directory factory) creates a commit's cached files after the directory fsync, so an OS crash can leave an index that will not open until the newest segments_N is deleted by hand, but only on file systems that persist a rename before earlier creates, which ext4 and XFS are not expected to do.
- Impact class
- index unusable
- Impact note
- Applications that write through NRTCachingDirectory (opt-in and @lucene.experimental, but the setup its class javadoc recommends for near-real-time search; Solr's _default configset uses NRTCachingDirectoryFactory, and whether Solr's commit path is affected was not checked) whose machine has an OS crash, kernel panic or power loss inside IndexWriter.commit(), in the window between the segments_N rename and the directory fsync that follows it, on a file system that persists that rename before earlier creates in the same directory (allowed by POSIX and by the model Lucene adopted in LUCENE-8048; ext4 data=ordered and XFS are not expected to reorder like this; no physical file system was tested), are left, at any index size and in every release since 7.2.0, with an index that DirectoryReader.open, DirectoryReader.listCommits and new IndexWriter all refuse (CorruptSegmentInfoException on the newest segments_N) until someone deletes that segments_N by hand, which recovers the previous commit; only the in-flight commit is lost.
- Summary
- You index through NRTCachingDirectory (for example new NRTCachingDirectory(FSDirectory.open(path), 5.0, 60.0), the class javadoc's own example), call IndexWriter.commit(), and the machine crashes during that commit. You expect what Lucene's commit protocol and LUCENE-8048 are built for: after the crash the index opens to the previous commit or to the new one, because SegmentInfos.prepareCommit fsyncs the directory before the commit is published by renaming pending_segments_N to segments_N. Instead the newly flushed segment files are still in NRTCachingDirectory's RAM cache at that directory fsync; NRTCachingDirectory.sync creates them on disk only afterwards and fsyncs their bytes but never the directory, so the rename can become durable before the new files' names do, and in that crash state every open fails (CorruptSegmentInfoException: segment info file _1.si cannot be read), while the same steps on a plain FSDirectory open fine.
- Introduced
- 4a5900728db6a84b4aea42545567a393989f25cf (master, merged by 0688be6c6677) and 2bfe7ed0f2f9 (branch_7x, the copy that shipped first, in 7.2.0): LUCENE-8048 'Filesystems do not guarantee order of directories updates' (committed by Erick Erickson from the JIRA patch; no PR; migrated as #9096) [read: git blame of SegmentInfos.java:970 and git log -S in the full-history clone] [exec: the 7.1.0 / 7.2.0 boundary]. An incomplete fix, not a regression: it added the dir.syncMetaData() barrier at the start of SegmentInfos.prepareCommit, which IndexWriter.startCommit calls before directory.sync(filesToSync) (both lines b83dde502bc8, 2012), while NRTCachingDirectory.sync, which creates the cached files in the delegate inside that sync call (unCache -> copyFrom), has had the same logic since the class was added (LUCENE-3092, 3525ba940fa2 on trunk and 8035a5ecf0f on branch_3x, 2011-05-17) and has never called syncMetaData (git log --all -S syncMetaData on NRTCachingDirectory.java: no commit) (2017-12-05)
- First affected version
- 7.2.0 [exec] (release tag 2017-12-19; CHANGES 7.2.0 'Bug Fixes' lists LUCENE-8048). The 18 releases 6.2.0 to 7.1.0 [exec] fail the same crash test, but so does the plain-FSDirectory control, because no directory fsync preceded the rename at all (LUCENE-8048 itself), so they are not counted as this bug; 6.1.0 and earlier: no verdict (the crash-model hooks Directory.rename and Directory.syncMetaData arrived in 6.2.0 with LUCENE-7373; 6.1.0 does not compile)
- Affected versions
- every release from 7.2.0 through 10.5.2, 10.6.0 (run later in the bug doc's checks, see below) and main ea57360912ed. [exec] all 71 Maven Central releases from 7.2.0 to 10.5.2 with two independently written JUnit crash-model repros (the finder's, seed DEADBEEF, and the independent reproduction's, seed C0FFEE01): in each, the NRTCachingDirectory second-commit crash state cannot be opened while the plain-FSDirectory control and a never-caching NRTCachingDirectory(0.0, 0.0) control open with all documents; main fails under -ea and -da, and with the fix shadowed main passes. The wrapping exception differs by version (CorruptIndexException 'Unexpected file read error while reading index' on 7.2.0-10.5.2, CorruptSegmentInfoException on main); the cause is always NoSuchFileException for the new segment's .si. [read] not fixed upstream: NRTCachingDirectory.sync still has no syncMetaData on main, branch_10x, branch_10_6 and branch_9_12 (read-only check 2026-10-26); 10.6.0, released from branch_10_6 on 2026-10-29, is affected too: the bug doc's minimal test fails 1 of 1 on the 10.6.0 release jars with -ea and -da [exec, 2026-10-30]. History [read, not executed]: in 4.8-4.10 FSDirectory.sync fsynced the directory after the files (LUCENE-5588), which also covered the files NRTCachingDirectory had just created; 5.0 (LUCENE-5925) moved that directory fsync after the rename, opening the gap for every Directory, and 7.2 (LUCENE-8048) closed it only for files that already exist at prepareCommit. Before 5.0 the commit protocol had no rename and is not analysed. All runs on aarch64 with JDK 25.0.3.
- 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
- semi-formal: exhaustive check; also reached by TLA+/TLC
- Upstream and prior reports
- none found (2026-10-26, read-only, plus the finder's and the independent reproduction's and the intended-behaviour check's searches: a scripted prior-report search with 16 GitHub issue/PR queries on apache/lucene, 3 commit queries, 2 downstream-repo queries over Elasticsearch, OpenSearch and Solr, 20 mailing-list queries over dev@, java-user@, issues@ and commits@, 4 JIRA-archive queries and 6 web queries; manual GitHub searches on apache/lucene (open PRs naming NRTCachingDirectory, syncMetaData, finishCommit, startCommit or SegmentInfos), apache/solr, OpenSearch, Elasticsearch and Lucene.NET; JIRA REST on LUCENE and SOLR; Solr lists; full reads of LUCENE-8048 (#9096, 24 comments) and of #7820, #7009, #8243, #8649, #8713, #6054, #10906, #10928 and #14334). Parent issue: LUCENE-8048 / #9096 (closed, fixed in 7.2), the same failure mode and crash model for plain directories; its thread never mentions NRTCachingDirectory. Symptom-only overlap: open #7820 / LUCENE-6762 and its closed duplicate #7009 (a segments_N referencing a missing .si, on Windows with plain MMapDirectory, cause undiagnosed; both about CheckIndex recovery), open PRs #12872 and #16474 (CheckIndex diagnostics and exorcise), #10906 (disk full). No open PR touches NRTCachingDirectory.sync, syncMetaData or finishCommit; NRTCachingDirectory.java is unchanged upstream since #15681 (2026-02).
- Lucene commit tested
- main
645d4fe14a20(2026-10-28; upstream main716d6f216f52has no change since toNRTCachingDirectory.java,SegmentInfos.javaorIndexWriter.java): the minimal test fails through gradle in scratch worktrees with assertions on and off (the assertions-on run repeated in the v2 re-run; both again in the v2 fact check), and passes with fix A and with fix B, as do 816 tests in 34 store and commit suites with each fix and, in the v2 re-run with fix A, 89 tests in the minimal test and the 5 closest store and commit suites (0 failures; 61 with assertions off). The minimal test also fails on the released 10.6.0 (assertions on and off), 10.5.2, 9.12.3 and 9.1.0 jars, and on main with seedsDEADBEEF,1234ABCDandC0FFEE01. Earlier hunt runs: mainea57360912edanda9779ef3c193and all 71 Maven Central releases from 7.2.0 to 10.5.2 with the hunt's and an independent reproducer's longer tests (with plain-directory and never-caching controls).
- Finding record
lucene/core/src/java/org/apache/lucene/store/NRTCachingDirectory.java:47-48 (class javadoc: 'This is safe to use: when your app calls {IndexWriter#commit}, all cached files will be flushed from the cached and sync'd.'), :50-58 (example: new NRTCachingDirectory(fsDir, 5.0, 60.0)), :63 (@lucene.experimental), :169-178 (sync: unCache each file, then in.sync; no syncMetaData), :181-186 (rename also unCaches), :305-323 (unCache: in.copyFrom creates the file in the delegate at :319)lucene/core/src/java/org/apache/lucene/index/SegmentInfos.java:966-972 (prepareCommit: dir.syncMetaData() then write(dir), i.e. pending_segments_N), :996-1012 (finishCommit: dir.rename(src, dest), then dir.syncMetaData())lucene/core/src/java/org/apache/lucene/index/IndexWriter.java:4188-4201 (commit javadoc: syncs all referenced index files so that 'the index updates will survive an OS or machine crash or power loss'; Directory#sync 'should not return until the file contents and metadata are on stable storage'), :5974 (startCommit: toSync.prepareCommit(directory)), :5991-5993 (filesToSync = toSync.files(false); directory.sync(filesToSync))lucene/core/src/java/org/apache/lucene/store/Directory.java:105-112 (sync javadoc: writes 'moved to stable storage'; 'Lucene uses this to properly commit changes to the index, to prevent a machine/OS crash from corrupting the index')lucene/test-framework/src/java/org/apache/lucene/tests/store/MockDirectoryWrapper.java:339-340 (TODO: 'we should also mess with any recent file renames, file deletions, if syncMetaData was not called!!', why upstream crash tests miss this)lucene/core/src/test/org/apache/lucene/index/TestIndexWriterOnJRECrash.java:164-170 (LUCENE-4738: a crash during the first commit may leave a corrupt index by design; CheckIndex only when the last generation is > 1)SECURITY.md (application owns untrusted input; index files and configuration trusted; robustness and durability fixes go to public issues)
- The finding in 30 seconds
lucene/core/src/java/org/apache/lucene/index/SegmentInfos.java:620, (at 645d4fe14a20) :966-972, :996-1012lucene/core/src/java/org/apache/lucene/index/IndexWriter.java:5974, (at 645d4fe14a20) :5992-5993lucene/core/src/java/org/apache/lucene/store/NRTCachingDirectory.java:159-163, (at 645d4fe14a20) :169-178, :231-245, :305-323lucene/core/src/java/org/apache/lucene/store/FSDirectory.java:244-250 (at 645d4fe14a20)lucene/core/src/java/org/apache/lucene/store/Directory.java:104-119, (at 645d4fe14a20) :131lucene/test-framework/src/java/org/apache/lucene/tests/store/MockDirectoryWrapper.java:339-340, (at 645d4fe14a20) :526
- Who is affected
lucene/core/src/java/org/apache/lucene/store/NRTCachingDirectory.java:50-58, (at 645d4fe14a20) :63
- Why it looks unintended
lucene/core/src/java/org/apache/lucene/store/NRTCachingDirectory.java:47-48, (at 645d4fe14a20) :63lucene/core/src/java/org/apache/lucene/index/IndexWriter.java:4188-4201 (at 645d4fe14a20)lucene/core/src/java/org/apache/lucene/store/Directory.java:104-112, (at 645d4fe14a20) :131lucene/test-framework/src/java/org/apache/lucene/tests/store/MockDirectoryWrapper.java:339-340 (at 645d4fe14a20)lucene/core/src/test/org/apache/lucene/index/TestIndexWriterOnJRECrash.java:164-170 (at 645d4fe14a20)
- Suggested fix
lucene/core/src/java/org/apache/lucene/index/IndexWriter.java:3721-3725 (at 645d4fe14a20)lucene/core/src/java/org/apache/lucene/index/SegmentInfos.java:996-1017 (at 645d4fe14a20)lucene/core/src/java/org/apache/lucene/store/NRTCachingDirectory.java:305-323 (at 645d4fe14a20) (unCache; :313 'Another thread beat us'); lucene/core/src/java/org/apache/lucene/index/IndexWriter.java:3775-3935 (synchronized (commitLock) around startCommit at :3915)
- TestNRTCachingDirectoryCommitCrash.java (one test):
repro/LFV-205/src/org/apache/lucene/store/TestNRTCachingDirectoryCommitCrash.java(70 lines)
The crash state it shows is the modelled one (LUCENE-8048's out-of-order directory updates), not a physical crash.
assets/LFV-205/bug.png(663 KB)
