All findings · Lucene finding · LFV-1090 · finishdocuments-slice-reset-skips-unapplied-deletes

After a non-tragic addDocument failure, the next document resets the delete slice and the next commit silently lacks an acknowledged delete

When addDocument fails because an in-memory segment could not finish copying its queued deletes and doc-values updates, the writer stays open and the segment's next document drops the uncopied rest. The next commit() succeeds with a deleted document still in it (9.8.0 through 10.6.0 and main).

Summary

Priority
Medium, rank 111Durable silent data loss on the default IndexWriter in every release from 9.8.0 to 10.6.0 and on main. A non-tragic failure while an in-memory segment copies its backlog of buffered deletes and doc-values updates leaves the writer open, and the next successful commit lacks an acknowledged delete, which keeps it in Medium. Its known triggers are contrived: about 2 GiB of buffered update or delete terms must pass one idle in-memory segment with no flush, refresh or commit.
Impact
data corruption
Since
9.8.0 (2023, exposed by #12392); the failure handling dates from 2011-2013introduced 2023-06-28 · about 3 years
Affected
9.8.0 through 10.6.0 and main
Status
documented
Found by
code reading next to the upstream fix #16867, witnessed by a public-API probe, checked with a bounded exhaustive model

AI-assisted page: this finding was first signalled by code reading or testing and then checked with AI tools; the checks it went through, and what each found, are under Prior reports, history and checks. Not yet triaged by a human. Snapshot of 2026-11-06.

The finding in 30 seconds

Each time an in-memory segment indexes a new document, it then applies to its earlier documents every delete and update queued since its last document. If that copy throws part-way, the rest stays queued. The segment's next document finds no new operations and calls reset(), which marks the whole queue done. The uncopied delete never reaches the document.

How deletes reach buffered documents, in one minute

  1. Each indexing thread fills its own in-memory segment (a DWPT). Deletes and doc-values updates go into one writer-wide queue, and every call returns a sequence number.
  2. Each in-memory segment keeps a slice of that queue: everything queued since its last document. Each time it indexes a new document, it then applies the slice to the documents it buffered before, node by node. That is how a delete reaches a document that is still in memory.
  3. commit() also returns a sequence number: every operation with a smaller or equal number is in the commit.
A flow diagram on a light card: the application sends deletes and updates into one ordered delete queue and new documents into a per-thread writer; the queue feeds a shared buffer that serves segments already written, and it feeds the per-thread writer's slice of the queue (from sliceHead to sliceTail), which the writer handles at its next document in two steps, 1. copy the slice into its own buffer and 2. mark the slice done (reset()), before its own documents are flushed as a new segment; a red ring and cross mark step 1, with the note that if step 1 throws part-way the writer stays open, and at the following document step 2 marks the whole slice done, so the deletes it never copied never reach its documents; two panels below contrast the expected outcome (the next commit applies every delete it acknowledged, or the writer closes itself) with the actual one (the writer stays open and the next commit still holds a document whose delete was acknowledged).
Deletes reach documents still in RAM only through their per-thread writer's slice, and after a copy that failed part-way the next document marks the whole slice done, so the deletes it never copied are skipped. Generated with AI, checked against the code.
The minimal repro, one thread, executed on main: a buffered document waits in an in-memory segment; 2,200,000 doc-values updates and the delete of the buffered document (sequence number 2,200,002) are queued; the next addDocument copies the queue into the segment, overflows at 2 GiB and throws ArithmeticException, leaving the delete uncopied while the writer stays open; the following addDocument resets the slice and drops it; the commit (sequence number 2,200,005) still has the buffered document. Expected 0 hits, actual 1. The fix makes the failure close the writer.
The minimal repro step by step, one thread: what the test calls, the segment's queue slice before and after the failed copy and the reset, then expected versus actual and the fix. Select the still to open it at full size. Generated with AI, checked against the code.

The finding, step by step

  1. Add a document buffered; it waits in its in-memory segment. Then call updateNumericDocValue 2,200,000 times with one 1 KB term, and deleteDocuments(id:buffered) (sequence number 2,200,002). ramBytesUsed() reports 2.6 MB.
  2. addDocument(next) throws ArithmeticException: integer overflow: after indexing next, the segment copies the 2.2 million queued operations into its own buffer, for the documents buffered before next, and passes the 2 GiB a byte pool can address. The delete, queued last, is never copied. isOpen() is true, getTragicException() null.
  3. addDocument(later) finds no new operations and marks the segment's queue as done, the uncopied part included.
  4. You expect the writer to close itself, or commit() (sequence number 2,200,005) to reflect the delete: 0 hits for id:buffered.
  5. Instead the commit succeeds and id:buffered has 1 hit. Nothing reports the lost delete, and CheckIndex is clean.

Who is affected

Applications on the default config that keep indexing after one addDocument failed (its javadoc promises "the index will be consistent"). The executed triggers are extreme: about 2 GiB of update or delete terms behind one in-memory segment that receives no documents, with no commit, refresh or flush in between, which the writer's RAM accounting does not count.

Mechanism, terms and numbers846 words

After an in-memory segment (a DWPT) has indexed a new document, and before addDocument returns, DocumentsWriterPerThread.finishDocuments applies to the documents buffered before it every delete and doc-values update queued since the segment's last document, its slice of the writer-wide delete queue (DocumentsWriterPerThread.java:291, :300-301, :361-390). DeleteSlice.apply walks the slice node by node and empties it only after the last node (DocumentsWriterDeleteQueue.java:348-367). When a node throws, the slice keeps its unapplied nodes, and the exception is not aborting: finishDocuments runs after allDocsIndexed = true and outside any onAbortingException guard, so isOpen() stays true and getTragicException() null. On the segment's next document updateSlice finds no new operation, and finishDocuments calls deleteSlice.reset() (:385), which marks the unapplied nodes done. The segment flushes without them, and the next commit() returns a sequence number that covers an acknowledged delete it lacks. Since 9.8.0 a legal workload reaches the throw: #12392 made ByteBlockPool.nextBuffer throw ArithmeticException at 2 GiB (ByteBlockPool.java:199), and an idle segment's copy of 2.2 million doc-values updates on a 1 KB term gets there. On 9.7.0 the same workload closes the writer. Treating a throw out of finishDocuments as aborting, as the indexing chain does for a byte-pool overflow while indexing, fixes it.

The code

// DocumentsWriterPerThread.finishDocuments, main 5adff78bb168      DocumentsWriterPerThread.java:361
if (deleteNode != null) {                                         // :374  updateDocument, delete by term
  seqNo = deleteQueue.add(deleteNode, deleteSlice);               // :375
  deleteSlice.apply(pendingUpdates, docIdUpTo);                   // :377
  return seqNo;
} else {
  seqNo = deleteQueue.updateSlice(deleteSlice);                   // :380  negative: new operations arrived
  if (seqNo < 0) {
    seqNo = -seqNo;
    deleteSlice.apply(pendingUpdates, docIdUpTo);                 // :383  throws here (addDocument(next))
  } else {
    deleteSlice.reset();                                          // :385  drops what :383 never applied
  }
}

// DocumentsWriterDeleteQueue.DeleteSlice.apply                   DocumentsWriterDeleteQueue.java:348
Node<?> current = sliceHead;
do {
  current = current.next;
  current.apply(del, docIDUpto);                                  // :364  one node per delete or update
} while (current != sliceTail);
reset();                                                          // :366  only after the last node
// reset(): sliceHead = sliceTail;                                // :369-372

Step by step (the repro)

  1. addDocument(buffered) leaves the thread's in-memory segment with one document and an empty slice.
  2. Each updateNumericDocValue call and the deleteDocuments call append one node to the writer-wide queue. The writer-wide buffer is frozen and applied whenever it reaches the 16 MB RAM buffer, but the segment makes its own copy only at its next document, so its slice spans all 2,200,001 nodes. Nothing counts that backlog (LFV-1077): ramBytesUsed() reads 2,555,912 bytes.
  3. addDocument(next): updateSlice moves sliceTail to the queue tail and returns a negative sequence number (DocumentsWriterDeleteQueue.java:284-293). apply copies every update, term bytes included, into the segment's private BufferedUpdates (BufferedUpdates.java:134-144). About 2 GiB in, ByteBlockPool.nextBuffer throws at byteOffset = Math.addExact(byteOffset, BYTE_BLOCK_SIZE) (ByteBlockPool.java:199). Executed stack, from there up to finishDocuments: ByteBlockPool.nextBuffer:199 <- ByteBlockPool.append:301 <- ByteBlockPool.append:228 <- BytesRefArray.append:70 <- FieldUpdatesBuffer.append:215 <- FieldUpdatesBuffer.addUpdate:184 <- BufferedUpdates.addNumericUpdate:139 <- DocValuesUpdatesNode.apply:507 <- DeleteSlice.apply:364 <- DocumentsWriterPerThread.finishDocuments:383. apply never reaches its reset(), so the slice still holds the rest of the updates and the delete of buffered.
  4. The exception is not aborting: maybeAbort (DocumentsWriterPerThread.java:310, :594-605) acts only on an exception passed to onAbortingException, and nothing passes this one. DocumentsWriter.updateDocuments skips doAfterDocument (DocumentsWriter.java:434) and returns the segment to the pool (:444). Executed: isOpen()=true, getTragicException()=null, numRamDocs()=2.
  5. addDocument(later): nothing was queued since step 3, so updateSlice returns a positive number and finishDocuments calls reset() (:385): sliceHead = sliceTail. The delete of buffered is gone from the segment. doAfterDocument then sees the segment's 2 GiB of pending updates and flushes it (executed: numRamDocs() 2 -> 0).
  6. A delete reaches a document that is still buffered only through its segment's slice: the delete queue's javadoc says each DWPT keeps its own head of the queue, for "its segments private deletes" (DocumentsWriterDeleteQueue.java:34-38), while the writer-wide slice records deletes "against all prior (already written to disk) segments" (:73-75). Executed: the committed index still has buffered. commit() returns 2,200,005.

Why the reset() is the defect, not the overflow

  • In normal operation that reset() changes nothing: after every successful apply the slice is already empty (:366), and a positive updateSlice means nothing was queued. It acts only after a failed apply; a bounded exhaustive model of the slice protocol found no other path within its bound (every sequence of up to 7 operations on one modelled segment).
  • One transient fault is enough: a fault-injection check that makes one queue node throw once (no overflow, 128 MB heap, through the real IndexWriter/DWPT path) loses the delete the same way (delete sequence number 3 missing from commit 7). It injects the fault through package-private writer internals, so it is evidence for the mechanism, not a public-API repro.
  • Read, not executed: for updateDocument and delete by term, add(node, slice) sets slice.sliceTail (DocumentsWriterDeleteQueue.java:166) before tryApplyGlobalSlice() (:168), so a throw there (the writer-wide buffer's overflow) leaves the same unapplied slice for the same reset(). On that path later freezes of the broken writer-wide buffer fail too, so a loss would not be the only symptom.
Exposure and likelihood421 words
  • What is lost. Deletes, and by code also doc-values updates, of documents buffered in that in-memory segment that were queued behind the failing node. Deletes acknowledged after the failure are applied (executed with the delete-term trigger, 75,000 distinct 30,000-byte delete terms: of three acknowledged deletes, buffered before the failure and b2, b3 after it, only buffered, queued behind the failing node, is missing). CheckIndex is clean, on that commit and on the commit of this doc's doc-values workload, but it checks structure only; it does not see a missing delete. By code, deletes of documents in already-flushed segments go through the writer-wide buffer, not this slice.
  • Silent. The exception surfaces on a later addDocument that has nothing to do with the delete, and the writer's own health signals say nothing is wrong: isOpen() is true and getTragicException() null. The addDocument javadoc promises that after an exception "the index will be consistent, but this document may not have been added" (IndexWriter.java:1465-1466); it does not tell the caller to stop using the writer or to roll back.
  • Default vs opt-in. An otherwise default IndexWriterConfig (the test pins the default codec with TestUtil.getDefaultCodec()), a plain ByteBuffersDirectory, one thread, assertions on or off. The workload calls only public IndexWriter API; the test class itself uses the test framework (LuceneTestCase, TestUtil).
  • Scale, the counterweight. About 2^31 bytes of buffered update or delete terms must pass one in-memory segment that holds documents but receives none, with no commit, NRT reopen or flush in between. Executed: 2.2 million updateNumericDocValue calls on one 1 KB term (3 GB heap), or 75,000 deleteDocuments calls with distinct 30,000-byte terms (8 GB heap, 9.9.0 and later). The writer's accounting does not count the backlog (LFV-1077), and neither safeguard stops the private copy: when buffered deletes reach the 16 MB RAM buffer the writer freezes and applies its writer-wide buffer (FlushByRamOrCountsPolicy.java:45-53), which does not touch the idle segment's slice; the flush-by-RAM check and the 1,945 MB per-thread hard limit (IndexWriterConfig.java:93) both run in doAfterDocument (DocumentsWriterFlushControl.java:215-251), which is reached only after finishDocuments returned (DocumentsWriter.java:428, :434). They first see the 2 GiB copy at addDocument(later), after the reset, and the segment is flushed then. Likelihood: contrived.
  • The same throw site in production (related, not a report of this loss): elastic/elasticsearch#160939 (Lucene 10.5.1) shows the same non-tragic ArithmeticException out of DeleteSlice.apply inside finishDocuments, on the writer-wide-buffer path; its reporter says it is still hitting production.

Minimal repro

One JUnit test, one thread, an otherwise default IndexWriterConfig, public IndexWriter API only; @Monster (3 GB heap, about 4 s). After the expected ArithmeticException it passes if the writer closed itself or the next commit lacks buffered. On main the commit still holds it (1 hit), assertions on and off; with the fix it passes.

TestDeleteLostAfterFailedAddDocument.java (one test, lucene/core)63 lines
/*
 * 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.document.Field;
import org.apache.lucene.document.NumericDocValuesField;
import org.apache.lucene.document.StringField;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.TermQuery;
import org.apache.lucene.store.ByteBuffersDirectory;
import org.apache.lucene.store.Directory;
import org.apache.lucene.tests.util.LuceneTestCase;
import org.apache.lucene.tests.util.LuceneTestCase.Monster;
import org.apache.lucene.tests.util.TestUtil;
import org.apache.lucene.util.BytesRef;

@Monster("buffers about 2.2 GB of doc-values updates; needs -Xmx3g")
public class TestDeleteLostAfterFailedAddDocument extends LuceneTestCase {

  public void testDeleteSurvivesFailedAddDocument() throws Exception {
    try (Directory dir = new ByteBuffersDirectory();
        IndexWriter w =
            new IndexWriter(dir, new IndexWriterConfig().setCodec(TestUtil.getDefaultCodec()))) {
      w.addDocument(doc("buffered"));
      Term hot = new Term("id", new BytesRef(new byte[1024]));
      for (int i = 0; i < 2_200_000; i++) {
        w.updateNumericDocValue(hot, "counter", i);
      }
      w.deleteDocuments(new Term("id", "buffered"));
      expectThrows(ArithmeticException.class, () -> w.addDocument(doc("next")));
      if (w.isOpen()) {
        w.addDocument(doc("later"));
        w.commit();
        try (DirectoryReader r = DirectoryReader.open(dir)) {
          // expected: the open writer's commit includes the earlier delete (0 hits); actual: 1 hit
          assertEquals(0, new IndexSearcher(r).count(new TermQuery(new Term("id", "buffered"))));
        }
      }
    }
  }

  private static Document doc(String id) {
    Document doc = new Document();
    doc.add(new StringField("id", id, Field.Store.NO));
    doc.add(new NumericDocValuesField("counter", 0));
    return doc;
  }
}
Command
cd $LUCENE
cp $REPRO/TestDeleteLostAfterFailedAddDocument.java lucene/core/src/test/org/apache/lucene/index/
./gradlew :lucene:core:test --tests org.apache.lucene.index.TestDeleteLostAfterFailedAddDocument -Ptests.monster=true -Ptests.heapsize=3g -Ptests.seed=DEADBEEF
# assertions off fails the same way: add -Ptests.asserts=false
Expected
The writer has closed itself after the failed addDocument, or the next commit has 0 hits for id:buffered.
Observed
AssertionError: expected:<0> but was:<1>: the commit (sequence number 2,200,005) still holds buffered, assertions on and off.

All files of this repro: repro/LFV-1090/

More files, environment, full output281 words

The codec is pinned with TestUtil.getDefaultCodec(), the directory is a plain ByteBuffersDirectory, and nothing on the failing path is randomized, so the mechanism does not depend on the seed: the recorded runs use DEADBEEF, and an independent check also failed it with seeds CAFEBABE and 12345678. The test sits in org.apache.lucene.index like the upstream tests and passes :lucene:core:check -x test (formatting, ecj lint, forbidden APIs). Run it with -Ptests.monster=true -Ptests.heapsize=3g.

On the released 9.7.0 it stops at expectThrows: there addDocument(next) throws ArrayIndexOutOfBoundsException: Index -65536 instead and the writer closes itself, so nothing is lost silently.

Regression test: this same test. It passes with the recommended fix. Because it expects the ArithmeticException, it tests fixes that keep the exception and close the writer (fix A); a fix that prevents the overflow (fix C) or throws another exception type would stop at expectThrows and would need that line changed.

Environment

lucenemain 5adff78bb168 (12.0.0-SNAPSHOT); the same test also fails on the released 10.6.0 and 9.8.0, assertions on and off
jdkOpenJDK 25.0.3
osLinux aarch64

Observed output

TestDeleteLostAfterFailedAddDocument > testDeleteSurvivesFailedAddDocument FAILED
    java.lang.AssertionError: expected:<0> but was:<1>
        at __randomizedtesting.SeedInfo.seed([DEADBEEF:52162A1781E7F99A]:0)
        at org.junit.Assert.fail(Assert.java:89)
        at org.junit.Assert.failNotEquals(Assert.java:835)
        at org.junit.Assert.assertEquals(Assert.java:647)
        at org.junit.Assert.assertEquals(Assert.java:633)
        at org.apache.lucene.index.TestDeleteLostAfterFailedAddDocument.testDeleteSurvivesFailedAddDocument(TestDeleteLostAfterFailedAddDocument.java:51)

# the same steps as a plain main() (numbers only, assertions off; the probe prints only these 3 stack frames, frames filtered)
addDocument(buffered) seqNo=1
2,200,000 updateNumericDocValue calls: last seqNo=2200001 ramBytesUsed=2555912 (max sampled 15954696)
deleteDocuments(id:buffered) seqNo=2200002
addDocument(next) threw java.lang.ArithmeticException: integer overflow
    at org.apache.lucene.util.ByteBlockPool.nextBuffer(ByteBlockPool.java:199)
    at org.apache.lucene.index.DocumentsWriterDeleteQueue$DeleteSlice.apply(DocumentsWriterDeleteQueue.java:364)
    at org.apache.lucene.index.DocumentsWriterPerThread.finishDocuments(DocumentsWriterPerThread.java:383)
after the failure: isOpen=true tragic=null numRamDocs=2
addDocument(later) seqNo=2200004 numRamDocs=0
commit() seqNo=2200005
committed index: numDocs=3 maxDoc=3 buffered=1 next=1 later=1   (the delete of buffered expects buffered=0)

# with fix A
after the failure: isOpen=false tragic=java.lang.ArithmeticException: integer overflow

Why it looks unintended

  • commit(): "All sequence numbers <= this value will be reflected in the commit". It returns 2,200,005 but lacks the delete (2,200,002).
  • addDocument on an exception: "this document may not have been added". Here an earlier, acknowledged delete is lost.
  • The same byte-pool overflow, hit while indexing a document's terms, already closes the writer (LUCENE-5987: no silent data loss).
The full argument431 words
  • commit(). "it returns the sequence number such that all indexing operations prior to this sequence will be included in the commit point" and "All sequence numbers <= this value will be reflected in the commit, and all others will not." (IndexWriter.java:4202-4204, :4207-4209). The class javadoc introduces sequence numbers as "describing which changes are in the commit point and which are not" (IndexWriter.java:117-121).
  • addDocument. "if an Exception is hit (for example disk full) then the index will be consistent, but this document may not have been added." (IndexWriter.java:1465-1466). The exception may cost this document, not an earlier acknowledged delete.
  • The design rule. LUCENE-5987: "IndexWriter will now forcefully close itself on aborting exception (an exception that would otherwise cause silent data loss)." (lucene/CHANGES.txt:11614-11616). The indexing chain follows it for a byte-pool overflow while indexing a document's terms: "If we hit an exception in here, we abort all buffered documents since the last flush, on the likelihood that the internal state of the terms hash is now corrupt" (IndexingChain.java:2068-2073; catch (Throwable th) { onAbortingException(th); throw th; } at :2098-2100). finishDocuments is the gap. An older precedent: LUCENE-938 "Fixed cases where an unhandled exception in IndexWriter's methods could cause deletes to be lost." (lucene/CHANGES.txt:20501-20503).
  • The reset() was not written for this case. It came with LUCENE-5006 (trunk 75ab79f0aa3, 2013-08-19, "if we don't need to apply we must reset!"; release 4.5.0 got it through the branch_4x commit 8003cc78298), where it also skipped applying while the segment had no documents (applySlice = numDocsInRAM != 0). The block path (updateDocuments) got today's form, a reset only when nothing new was queued, in 76fb616bdf5 (2016); the single-document path kept applySlice = numDocsInRAM != 0 until LUCENE-9276 (#1346, c0cf7bb4b0e, Simon Willnauer, 2020-03-13) sent addDocument through finishDocuments. In today's form the branch changes nothing unless an apply failed. We found no comment, test or PR that discusses a failed apply (in the history of these lines, the test tree and the prior-report searches); TestDocumentsWriterDeleteQueue, the only test that touches DeleteSlice, applies successfully.
  • No documented limit was exceeded. Every term is 1 KB (the indexed-term limit is 32,766 bytes, IndexWriter.java:258), the IndexWriterConfig settings are the defaults, and ramBytesUsed() stays below 16 MB. The per-thread hard limit is "a safety limit to prevent a DocumentsWriterPerThread from address space exhaustion due to its internal 32 bit signed integer based memory addressing" (IndexWriterConfig.java:373-378); it is checked only after finishDocuments returns, so it cannot stop this copy.
The devil's-advocate check203 words

Unsupported scale. On #15330 a maintainer (rmuir) wrote that "the limits on IndexWriter are 2GB". That remark is about one document's postings pool, which the per-thread hard limit bounds. Here the application never sees a number near 2 GB, and #16867 treats the same 2 GB overflow of the writer-wide buffer as a bug to fix (lucene/CHANGES.txt:324-326). Scale goes to severity (contrived), not to validity.

"After a non-tragic failure, call rollback()." On open #9738 (2019) a maintainer (s1monw) said that non-tragic exceptions from some methods may lose documents and that a user can always call rollback(). Their list (getReader, deleteAll, addIndexes, flushNextBuffer, doFlush, prepareCommitInternal, startCommit) leaves out addDocument, and they describe unrecoverable exceptions during indexing as handled in DocumentsWriter. No javadoc tells a caller to roll back after a failed addDocument; its javadoc promises a consistent index.

The class javadoc promises a self-close only "if you hit an Error, or disaster strikes during a checkpoint" (IndexWriter.java:162-165). Staying open is not the defect; commit() then breaking its own contract is.

Verdict: not intended, a defect. An independent reviewer from another model family (OpenAI GPT-5.6) rated the intended-behaviour argument weak; the devil's-advocate verifier rejected each case above.

Suggested fix

Recommended A: a throw out of finishDocuments is aborting

Treat any exception out of finishDocuments as aborting (onAbortingException), as the indexing chain does for a byte-pool overflow: the segment is aborted, the writer closes itself and the last commit stays intact. Validated: the repro passes on and off; the org.apache.lucene.index tests (1,397) and :lucene:core:check pass.

long seqNo;
try {
  if (deleteNode != null) {
    ... unchanged ...
  } else {
    ... unchanged, including deleteSlice.reset() ...
  }
} catch (Throwable t) {
  // The slice may now hold deletes that were never applied to pendingUpdates, and a later
  // finishDocuments would reset() past them: this DWPT can no longer honour them.
  onAbortingException(t);
  throw t;
}
return seqNo;
All options and trade-offs3 options ยท 666 words

Validated with gradle in a clean scratch worktree of 5adff78bb168 (OpenJDK 25.0.3, seed DEADBEEF, 2026-11-03): the repro fails stock with assertions on and off and passes with fix A, on and off. With fix A and the test in place, 7 closest suites (153 tests), the whole org.apache.lucene.index test package (214 classes, 1,397 tests, 0 failures, 57 skipped: nightly, monster or otherwise gated; the monster repro ran separately) and :lucene:core:check -x test pass. The scratch tree was clean after reverting. No stress or performance run was made.

A: a throw out of finishDocuments is aborting (validated by the checks) (recommended)

Wrap the body of DocumentsWriterPerThread.finishDocuments in a try whose catch (Throwable t) calls onAbortingException(t) and rethrows. The finally of updateDocuments and updateBatch then calls maybeAbort, which aborts the segment and fires the tragic event (DocumentsWriterPerThread.java:594-605), and the writer closes itself. +17/-10 lines in one file, mostly the re-indented existing body. It mirrors the indexing chain (IndexingChain.java:2098-2100).

long seqNo;
try {
  if (deleteNode != null) {
    ... unchanged ...
  } else {
    ... unchanged, including deleteSlice.reset() ...
  }
} catch (Throwable t) {
  // The slice may now hold deletes that were never applied to pendingUpdates, and a later
  // finishDocuments would reset() past them: this DWPT can no longer honour them.
  onAbortingException(t);
  throw t;
}
return seqNo;

Trade-offs

The segment's buffered documents are lost with the abort (here buffered and next), and the application must open a new writer; the last commit stays the recovery point. That is the stance LUCENE-5987 took for every exception that would otherwise lose data silently. By code it also covers the writer-wide-buffer path (add(node, slice) -> tryApplyGlobalSlice, not executed). It makes any throw aborting, also an AlreadyClosedException from a closed delete queue, but a queue is closed only after every segment using it was flushed (DocumentsWriter.java:707), and the index test package passes. It also makes every other runtime failure out of finishDocuments aborting, where the writer used to stay open; the nearby suites and the index package pass, but no stress, concurrency, full-flush or performance test was run, so a reviewer may want the catch narrowed or argued case by case. Optional cleanup: the comment at the top of finishDocuments (DocumentsWriterPerThread.java:362-365) still describes the earlier order (update the slice, then increment the doc id); today the doc id is assigned first (:291).

What the checks validated

  • Repro: fails stock (assertions on and off), passes with A (on and off), through gradle on 5adff78bb168.
  • TestDocumentsWriterDeleteQueue, TestIndexWriterExceptions, TestIndexWriterExceptions2, TestIndexWriterDelete, TestNumericDocValuesUpdates, TestBinaryDocValuesUpdates, TestIndexWriterWithThreads: 153 tests, 0 failures, 10 skipped.
  • :lucene:core:test --tests 'org.apache.lucene.index.*': 214 classes, 1,397 tests, 0 failures, 57 skipped; :lucene:core:check -x test passes.
  • Numbers probe with A: isOpen=false, tragic exception the ArithmeticException.
  • Bounded exhaustive model of the slice protocol: 0 violations with this rule, 1,073 (transient fault) and 1,767 (persistent fault) without it.

B: recover without closing (not enough alone)

Apply instead of reset() when the slice is not empty, or let apply advance sliceHead node by node so a retry resumes at the failing node.

Trade-offs

The per-thread BufferedUpdates is left mid-update: nextBuffer installs the new block and advances bufferUpto before Math.addExact throws (ByteBlockPool.java:196-199), so the pool is left half-advanced and appending to it again is not safe; the segment would still have to be flushed or aborted. Read, not validated.

C: bound the idle segment's backlog (the trigger, LFV-1077)

Count the bytes an idle segment's slice pins, or flush such a segment early, so its copy never nears 2 GiB.

Trade-offs

Removes the executed per-thread trigger on a default config, and a maintainer may reasonably want it first. It leaves the handling, though: the writer-wide-buffer path and any future non-aborting throw from a queue node still reach the reset(), and the transient-fault check loses a delete with no overflow at all. Do it in addition to A, not instead. Not validated here.

Downstream note

An application that relies on deleteDocuments to withdraw a document (removed content, revoked access) could keep serving it after the next commit, but only after about 2 GiB of buffered update or delete terms reach one writer without a flush or commit.

How it was found

Code reading next to the recent upstream fix #16867, which caps the writer-wide delete buffer, looked for the per-segment copy of the same stream that #16867 leaves unbounded. A public-API probe then showed a delete missing from a commit after a non-tragic failure. It was reduced to one test and checked with a bounded exhaustive model of the slice protocol.

The method in full187 words

Method: non-formal (code reading witnessed by an executed public-API probe), confirmed with a bounded exhaustive model.

  • #16867 (merged 2026-10-30) caps the bytes of buffered deletes and updates in the writer-wide delete queue, because the byte pools use 32-bit offsets, and assumes the per-thread buffers cannot overflow. Reading the code around it for other copies of the same stream found the copy an in-memory segment makes in finishDocuments, which has no cap, and the non-aborting handling when it throws.
  • A public-API probe (large distinct delete terms behind an idle in-memory segment, then a delete of the buffered document, 8 GB heap) showed addDocument failing there, the writer staying open and the next commit missing the delete. The case was then reduced to the one 3 GB doc-values-update JUnit test of this page.
  • A bounded exhaustive model of one segment's delete slice (every sequence of up to 7 deletes, adds, updates and commits with one injected non-aborting fault: 19,420 fault scenarios per mode) found 1,073 (transient fault) and 1,767 (persistent fault) runs where a commit lacks an acknowledged operation, 0 with the fix and 0 without a fault.

Prior reports, history and checks

Prior reports

New: no report of this defect (GitHub issues and PRs incl. the JIRA archive, mailing lists, downstream trackers, the web; re-checked 2026-11-03). Related, different defects: elastic/elasticsearch#160939 (the same exception at the same call on the writer-wide buffer, triaged as no corruption) and #16867 (caps that buffer).

Searches5 searches

In the documented searches, no Lucene issue, PR, JIRA entry or list thread, and nothing outside Lucene, describes this defect (the reset() after a failed apply dropping deletes for buffered documents) or its consequence (an acknowledged delete missing from a later successful commit while the writer stays open). Related prior items, each about a different defect:

  • elastic/elasticsearch#160939 (closed 2026-10-30, Lucene 10.5.1): bulk items failed with the same non-tragic ArithmeticException: integer overflow out of DeleteSlice.apply inside finishDocuments, but on the writer-wide-buffer path (add(node, slice) -> tryApplyGlobalSlice) during a slow full flush; the reporter says it is still hitting production. Its subject is the overflow of the shared buffer, fixed upstream by #16867; its automated triage said "no data corruption (it fails fast)", and nobody looks at the per-thread slice, the reset() or a lost delete. elastic/elasticsearch#160855 (open) asks for better logging for the same incident.
  • apache/lucene#16867 (merged 2026-10-30, not in 10.6.0): caps the shared delete buffer and wires it into stall control. Its description says the indexing threads "have their own maximum buffer size of 1945Mb to avoid overflowing any internal data structures"; it does not change finishDocuments or DeleteSlice.
  • Also related: #9738 (LUCENE-8692, open: getTragicException() misses some corrupting exceptions; the rollback() stance), #15152 and #15330 (postings-pool overflow while indexing), #14261 (an OutOfMemoryError inside DeleteSlice.apply; an Error is tragic), #10080 (LUCENE-9037: a non-aborting exception, then a terms-hash pool overflow; resolved 2020), #7049 (LUCENE-5987, the self-close rule), #6327 (LUCENE-5263) and #5694 (LUCENE-4629), earlier "deletes lost after an exception" bugs with other mechanisms, fixed 2012-13.
What was searched, when, and what turned up
WhereWhenHowFound
GitHub issues, PRs and commits (apache/lucene incl. the migrated JIRA archive); Elasticsearch, OpenSearch, Solr; dev@, java-user@, issues@, commits@; JIRA; web2026-11-02two scripted prior-art runs (51 and 45 queries) plus manual GitHub, JIRA, list and web searchesonly the related items above
Open PRs2026-11-02the changed lines of all 410 open apache/lucene PRsnone changes finishDocuments, DeleteSlice, updateSlice or reset()
Re-check (two independent passes)2026-11-03the items called related earlier, re-read against this defectrelated but about different defects
This doc: scripted run; apache/lucene items; downstream issues2026-11-03a scripted run of 45 queries (16 GitHub, 3 commit, 2 downstream, 20 list, 4 JIRA; 425 hits, the 5 closest read) and one web search; every apache/lucene issue and PR updated since 2026-11-02 (52) and the changed files of the open ones; the comments of elastic/elasticsearch#160939 and #160855nothing new: the closest hits are a sequence-number assertion race (#13571), LUCENE-9037 (#10080), the 2010 per-thread segment design (#3400), an IndexWriter refactoring (#3101) and a 2015 dev@ question about overlapping slices; only #16847 among the open PRs touches DocumentsWriterPerThread.java, not these lines; no new downstream comment
apache/lucene issues and PRs; downstream issues2026-11-03 (after the doc's review)GitHub searches for "DeleteSlice", "finishDocuments" and "delete slice reset"; every apache/lucene item updated since a recent cutoff; elastic/elasticsearch#160939 and #160855nothing new: only #14261, #13352, #13571 and unrelated word matches; the updated items (#16977, #16969) are unrelated; no downstream change since 2026-10-30

History

Users can hit it since 9.8.0: #12392 (2023, correct in itself) made the 2 GiB overflow throw inside finishDocuments; on 9.7.0 the same workload closes the writer. The apply-then-reset dates from 2011, the reset() branch from 2013. Affected through 10.6.0 and main; not fixed upstream as of 2026-11-03.

Versions tested7 rows

Exposing change: #12392 "Catch offset overflows in byte pool (#9660)" (Stefan Vodita, 2023-06-28; main b88d3e19889, 9.x backport 445b30d421c, first released in 9.8.0) changed byteOffset += BYTE_BLOCK_SIZE to byteOffset = Math.addExact(byteOffset, BYTE_BLOCK_SIZE): a correct fix that turns a silent int wrap into an exception, which for this workload is thrown inside finishDocuments. On the 9.7.0 jars with only that ByteBlockPool.java swapped in, the loss appears. The handling itself: DeleteSlice.apply resets only after every node succeeds since LUCENE-2956 (e95097a7c8d, Simon Willnauer, 2011-04-13, release 4.0.0); the no-new-deletes reset() dates from LUCENE-5006 (trunk 75ab79f0aa3, 2013-08-19; in release 4.5.0 through the branch_4x commit 8003cc78298). Its current form came for the block path with 76fb616bdf5 (2016) and for addDocument with LUCENE-9276 (#1346, c0cf7bb4b0e, 2020-03-13), which sent single documents through finishDocuments. From 4.5.0 to 9.7.0 no legal-input, non-tragic exception out of DeleteSlice.apply is known. Not fixed upstream: on upstream main f72004515ec (2026-11-03), branch_11x and branch_10_6 the last commits on the files involved predate 5adff78bb168.

Versions and branches executed, with results
Version or commitVerdictResult
4.5.0 to 9.7.0 latentthe handling is the same, but no legal-input, non-tragic exception out of DeleteSlice.apply is known
9.7.0 fails loudlythis workload: addDocument(next) throws ArrayIndexOutOfBoundsException -65536 and the writer closes itself; nothing lost
9.7.0 + ByteBlockPool.java of #12392 commit checkonly that file swapped in: the delete is lost
9.8.0 first: 9.8.0this test fails, assertions on and off
9.9.0, 9.12.3, 10.5.2 affectedan earlier form of the test or an independent test fails; the distinct-delete-term trigger fails from 9.9.0 (#12573)
10.6.0 affectedthis test fails, assertions on and off
main 5adff78bb168 affectedthis test fails through gradle, on and off; passes with fix A

Independent checks

An independent reproducer, two executed reviews by another model family (OpenAI GPT-5.6), a devil's-advocate verifier, an adjudication and a fault-injection check all rated it real with their own runs; this doc's gradle runs reproduced it on main 5adff78bb168 and validated the fix.

Checks and verdicts245 words

The earlier checks ran on main e1faeba08bde (one review on ea57360912ed); the files on this path are byte-identical to 5adff78bb168.

Each independent check and its verdict
CheckVerdictWhat it found
finder foundOne-test JUnit fails on main, assertions on and off; the bounded exhaustive model: violations only with a fault and without the fix.
independent reproducer realOwn test fails on main (on, off), 10.6.0, 10.5.2, 9.12.3 and 9.8.0; passes on 9.7.0 and with the fix; a diagnostic copy shows the reset() reached with a non-empty slice.
cross-model review (OpenAI GPT-5.6, executed) real (high)Its runs fail on and off and pass with the fix.
cross-model review, refute lens (OpenAI GPT-5.6, executed) real (high)Own test with a file-system directory fails on and off; its 1,900,000-update control passes; the fix passes. Rated the intended-behaviour argument weak.
devil's-advocate verifier real (medium-high)Rejected the scale, rollback and class-javadoc arguments; confirmed the mechanism with an internal-API probe at 256 MB.
adjudication confirmed (high), MediumRe-ran the test on a clean checkout: fails, assertions on and off.
fault-injection check realOne queue node that throws once, through the real writer path at 128 MB: the delete is lost the same way, so the reset() alone causes it.
this doc (gradle) reproduced, fix validatedThe test fails on main through gradle, on and off; with fix A it passes, and the index test package and :lucene:core:check -x test pass.

Diagram file

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.

Sources and provenance

Finding record, Lucene and upstream sources, repro files and diagram notes1,470 words

Finding record

LFV-1090, snapshot of 2026-11-06

Registry title
After addDocument fails with a non-tragic exception while an in-memory segment copies its backlog of buffered deletes and doc-values updates, the segment's next document resets its delete slice past the operations that were never applied, so IndexWriter stays open and the next successful commit silently lacks an acknowledged delete of a buffered document
Area
index / IndexWriter delete queue (DocumentsWriterPerThread.finishDocuments, DeleteSlice failure handling)
Priority
Medium, rank 111: Durable silent data loss on the default IndexWriter in every release from 9.8.0 to 10.6.0 and on main. A non-tragic failure while an in-memory segment copies its backlog of buffered deletes and doc-values updates leaves the writer open, and the next successful commit lacks an acknowledged delete, which keeps it in Medium. Its known triggers are contrived: about 2 GiB of buffered update or delete terms must pass one idle in-memory segment with no flush, refresh or commit.
Impact class
data corruption
Impact note
Hits applications on the default IndexWriterConfig, codec and directory that keep indexing after an addDocument failed with a non-tragic exception while the in-memory segment applied its backlog of buffered deletes and doc-values updates: the writer stays open (isOpen() true, getTragicException() null) and the next successful commit, whose sequence number covers it, silently lacks an acknowledged delete of a document buffered in that segment [exec; by code also any doc-values update of those documents queued behind the failing node, read]; the only executed triggers are extreme (about 2^31 bytes of buffered update or delete terms passing one idle in-memory segment with no flush, refresh or commit, for example 2.2 million updateNumericDocValue calls on one 1 KB term, which the RAM buffer never counts, LFV-1077); every release from 9.8.0 to 10.6.0 is affected, while 9.7.0 closes the writer instead.
Summary
You add a document to an IndexWriter, then send a long run of doc-values updates (executed: 2.2 million updateNumericDocValue calls selected by one 1 KB term) and delete the buffered document; the next addDocument fails with ArithmeticException 'integer overflow' while the in-memory segment copies that backlog into its own buffer, and the writer stays open with no tragic exception. You expect either the writer to close itself or the next successful commit, whose sequence number is above the delete's, to reflect the delete. Instead the following addDocument finds no new deletes and resets the segment's delete slice past the operations the failed copy never reached, so the commit succeeds and the deleted document is still in the index.
Introduced
b88d3e19889f (main) and its branch_9x backport 445b30d421c7 (first released in 9.8.0), apache/lucene#12392 'Catch offset overflows in byte pool (#9660)' by stefanvodita: the exposing change, correct in itself. Its Math.addExact in ByteBlockPool.nextBuffer turns the 2 GiB overflow of this workload into a non-tragic ArithmeticException thrown inside the per-thread DeleteSlice.apply that finishDocuments calls [exec: on the 9.7.0 jars with only ByteBlockPool.java from 445b30d421c7 swapped in the JUnit fails; plain 9.7.0 passes]. The handling defect itself is older [read: git log -S and git blame in the full-history clone]: DeleteSlice.apply resets only after every node succeeds since e95097a7c8d (LUCENE-2956 'Support updateDocument() with DWPTs', 2011-04-13, release 4.0.0), and finishDocument resets the slice when no new deletes arrived ('if we don't need to apply we must reset!') since 75ab79f0aa3 (LUCENE-5006, trunk, 2013-08-19; in release 4.5.0 through the branch_4x commit 8003cc78298); the current seqNo-based form comes from 76fb616bdf5 (2016-06-01, block path) and c0cf7bb4b0e (#1346, LUCENE-9276, 2020-03-13, addDocument path) (2023-06-28 (author date of b88d3e19889f and 445b30d421c7, the exposing change); the handling defect 2011-04-13 (e95097a7c8d) and 2013-08-19 (75ab79f0aa3))
First affected version
9.8.0 [exec: the JUnit fails; 9.7.0 passes because the same workload makes the writer close itself with a tragic ArrayIndexOutOfBoundsException, so nothing is lost silently]; the handling defect is latent from 4.5.0 through 9.7.0, with no legal-input, non-tragic exception out of DeleteSlice.apply known to us there [read]
Affected versions
[exec] the claimed JUnit fails on main e1faeba08bde (-ea and -da), 10.6.0 (released 2026-10-29, 4ae946e5a9b; -ea and -da), 10.5.2, 9.12.3, 9.9.0 and 9.8.0 (-da), always 'delete seqNo=2200002 must be in commit seqNo=2200005 ... expected:<0> but was:<1>'; the independent reproducer's own JUnit fails on main (-ea, -da), 10.6.0, 10.5.2, 9.12.3 and 9.8.0 (-da); 9.7.0 passes (-da: the writer closes itself). The second trigger (75,000 deletes of distinct 30,000-byte terms, 8 GB heap) fails on main (-ea, -da), 10.6.0, 10.5.2 and 9.9.0 and passes on 9.8.0 (#12573 moved the per-thread delete terms into a ByteBlockPool in 9.9.0). [read] every release tag from 9.8.0 to 10.6.0 has Math.addExact in ByteBlockPool.nextBuffer, the reset() in finishDocuments' no-new-deletes branch and a DeleteSlice.apply without failure handling. Not new in 10.6. Not fixed upstream: no commit on main (254ce48660d), branch_11x or branch_10_6 changes finishDocuments or DeleteSlice since 2026-09-01 (author dates checked), and no open PR changes those lines (2026-11-02)
Status
documented (brief, checks and a per-finding doc exist)
Verification
Confirmed by AI-run checks: an independent repro, a cross-model review that agreed, an intended-behaviour check, a prior-report search and a skeptical audit; not yet triaged by a person.
Found by
non-formal: code reading; also reached by exhaustive check
Upstream and prior reports
no report of this mechanism or its consequence found (2026-11-02, read-only, on top of the finder's, the intended-behaviour check's and the adjudication's searches, refreshed the same day; re-checked 2026-11-03): two scripted prior-report searches (51 and 45 queries over GitHub issues, PRs and commits, Elasticsearch, OpenSearch and Solr, the dev@, java-user@, issues@ and commits@ lists, the JIRA archive and the web), manual GitHub, JIRA, list and web searches, the changed files of all 410 open apache/lucene PRs (10 touch the fix-site files; none changes finishDocuments, DeleteSlice, updateSlice or reset()) and every PR updated since 2026-10-31 (only #16847 touches DocumentsWriterPerThread.java, not these lines). Related, by others: elastic/elasticsearch#160939 (closed 2026-10-30, Lucene 10.5.1) shows the same non-tragic ArithmeticException 'integer overflow' out of DeleteSlice.apply inside finishDocuments with the writer kept serving, on the shared-buffer path (DocumentsWriterDeleteQueue.add(node, slice) -> tryApplyGlobalSlice) during a slow full flush; its triage says 'no data corruption (it fails fast)' and nobody mentions the per-thread slice, the reset() or a lost delete; elastic/elasticsearch#160855 (open) is the same incident; apache/lucene#16867 (merged 2026-10-30, not in 10.6.0) caps the shared buffer and assumes the per-thread buffers cannot overflow. Related, not this bug: #9738, #15152, #15330, #14261, #10080, #7049 (LUCENE-5987), #5694, #6327, #7111, #13571, #13352 and #10316; not fixed upstream.
Lucene commit tested
main 5adff78bb168 (12.0.0-SNAPSHOT), 2026-11-03: the one-test repro through gradle in a clean scratch worktree, stock (fails, assertions on and off) and with fix A (passes, on and off); with fix A the 7 closest suites, the whole org.apache.lucene.index test package (214 classes, 1,397 tests, 0 failures) and :lucene:core:check -x test pass. The same test on the released 10.6.0 and 9.8.0 jars fails, on and off; on 9.7.0 the failing call throws ArrayIndexOutOfBoundsException instead. The earlier checks ran on main e1faeba08bde and ea57360912ed, whose files on this path are byte-identical to 5adff78bb168.

Lucene and upstream sources

Finding record
  • lucene/core/src/java/org/apache/lucene/index/DocumentsWriterPerThread.java:361-389 (finishDocuments: add + apply for a delete node; updateSlice, apply at :383 when new deletes arrived, otherwise reset() at :385)
  • lucene/core/src/java/org/apache/lucene/index/DocumentsWriterDeleteQueue.java:153-170 (add(node, slice) sets slice.sliceTail before tryApplyGlobalSlice), :284-293 (updateSlice: positive seqNo when sliceTail == tail), :348-372 (DeleteSlice.apply resets only after the last node; reset() sets sliceHead = sliceTail)
  • lucene/core/src/java/org/apache/lucene/util/ByteBlockPool.java:196-199 (nextBuffer installs the new buffer, then Math.addExact throws at :199)
  • lucene/core/src/java/org/apache/lucene/index/IndexWriter.java:117-121 (sequence numbers; commit's seqNo describes which changes are in the commit point), :1463-1467 (addDocument: 'the index will be consistent, but this document may not have been added'), :4208-4210 (commit: 'All sequence numbers <= this value will be reflected in the commit')
  • lucene/core/src/java/org/apache/lucene/index/IndexWriterConfig.java:373-378 (setRAMPerThreadHardLimitMB: 'a safety limit to prevent a DocumentsWriterPerThread from address space exhaustion due to its internal 32 bit signed integer based memory addressing')
The finding in 30 seconds
  • lucene/core/src/java/org/apache/lucene/index/DocumentsWriterPerThread.java:300-310, (at 5adff78bb168) :361-390, :594-605
  • lucene/core/src/java/org/apache/lucene/index/DocumentsWriterDeleteQueue.java:34-38, (at 5adff78bb168) :73-75, :153-172, :284-293, :333-372
  • lucene/core/src/java/org/apache/lucene/util/ByteBlockPool.java:187-200; (at 5adff78bb168) index/BufferedUpdates.java:134-144; index/DocumentsWriter.java:412-446
Who is affected
  • lucene/core/src/java/org/apache/lucene/index/IndexWriter.java:1462-1468; (at 5adff78bb168) index/IndexWriterConfig.java:83, :93, :373-378; index/FlushByRamOrCountsPolicy.java:45-53; index/DocumentsWriterFlushControl.java:215-251; index/DocumentsWriter.java:428, :434
  • https://github.com/elastic/elasticsearch/issues/160939
Why it looks unintended
  • lucene/core/src/java/org/apache/lucene/index/IndexWriter.java:117-121, (at 5adff78bb168) :162-165, :258, :1462-1468, :4202-4209
  • lucene/core/src/java/org/apache/lucene/index/IndexingChain.java:2068-2073, (at 5adff78bb168) :2098-2100; index/IndexWriterConfig.java:93, :373-378
  • lucene/CHANGES.txt:324-326 (at 5adff78bb168) (#16867), :11614-11616 (LUCENE-5987), :20501-20503 (LUCENE-938)
  • https://github.com/apache/lucene/pull/15330; https://github.com/apache/lucene/issues/9738; https://github.com/apache/lucene/issues/7049
Suggested fix
  • lucene/core/src/java/org/apache/lucene/index/DocumentsWriterPerThread.java:594-605; (at 5adff78bb168) util/ByteBlockPool.java:196-199; index/DocumentsWriter.java:707; index/IndexingChain.java:2098-2100
How it was found
  • https://github.com/apache/lucene/pull/16867
Prior reports
  • https://github.com/elastic/elasticsearch/issues/160939; https://github.com/elastic/elasticsearch/issues/160855; https://github.com/apache/lucene/pull/16867
History
  • GitHub commits API, apache/lucene main f72004515ec, branch_11x, branch_10_6 (last commits on DocumentsWriterPerThread.java, DocumentsWriterDeleteQueue.java, ByteBlockPool.java, BufferedUpdates.java, FieldUpdatesBuffer.java), 2026-11-03
Diagrams
  • lucene/core/src/java/org/apache/lucene/index/DocumentsWriterPerThread.java:361-390; (at 5adff78bb168) index/DocumentsWriterDeleteQueue.java:348-372

Other statements trace to the hunt's working files (models, logs, reviews and briefs), which are not published.

Repro files

About the diagrams

A step-by-step picture of the repro, made in parallel with this doc from the code at 5adff78bb168 and the executed runs; the PNG is the summary still.

Built 2026-11-06 from the hunt's record of this finding and its doc file. Lucene main under test in this hunt: ea57360912ed. Every listed finding was re-run on Lucene main e1faeba08bde on 2026-11-02.