m3l0n-sq opened a new issue, #4043:
URL: https://github.com/apache/jena/issues/4043

   ### Version
   
   6.1.0
   
   ### What happened?
   
   ## What happened?
   
   A long-running Fuseki instance with a TDB2 dataset started returning HTTP 500
   for every query touching one B+Tree index, and never recovered until the JVM
   was restarted. (See attached stack trace) 
   
   [Claude Code](https://claude.ai/code) performed an analysis of this issue 
and gave the below findings.
   
   This signature is usually read as on-disk index truncation/corruption from an
   unclean shutdown. In this case the database files were intact — the failure 
is
   in-memory state corruption inside `BlockAccessMapped`, and a restart fully
   clears it. 
   
   ## Analysis
   
   `BlockAccessMapped.getByteBuffer` slices a block out of the shared 
per-segment
   `MappedByteBuffer` by temporarily mutating the shared buffer's state:
   
   ```java
   segBuffer.position(segOff);
   segBuffer.limit(segOff + blockSize);   // shrinks the SHARED buffer's limit
   ByteBuffer dst = segBuffer.slice();
   segBuffer.limit(segBuffer.capacity()); // restore -- but NOT in a finally
   ```
   
   If any throwable escapes between the limit-shrink and the restore — e.g. an
   `OutOfMemoryError` raised at the `slice()` allocation under heap pressure —
   the shared segment buffer is left with a shrunken limit. Every subsequent
   access to a higher block in that segment then throws
   `IllegalArgumentException: newPosition > limit` from `Buffer.position(int)`,
   and the failure is self-sustaining: the poisoned call throws *before* 
reaching
   the line that would restore the limit.
   
   Note this poisoning is not confined to writes. There is one
   `MappedByteBuffer` per 8 MiB segment, shared by every block access to that
   segment. A read never changes the mapped bytes, but the old slicing technique
   mutates the shared buffer's *cursors* (`position`/`limit`) to steer `slice()`
   onto the target block, because the classic `slice()` always carves out
   `[position, limit)` and had no absolute offset/length overload before Java 
13.
   So `read` — the hot path taken by every query, via
   `BPTreeNode.get → BlockAccessMapped.read → getByteBuffer` — borrows and
   restores shared mutable state on each call, and is equally able to both 
create
   and hit the poisoned limit.
   
   Supporting evidence from the observed failure:
   
   - The reported limit `2588672` is not segment-aligned (segments are 8 MiB,
     mapped in full via `FileChannel.map(READ_WRITE, offset, SegmentSize)`, so a
     mapped buffer's capacity is always 8388608). `2588672 = 315 * 8192 + 8192`
     is exactly a leftover per-block slice limit — in-memory state, not a
     file-length artifact.
   - All failures in the incident showed the same `Seg` and the same stuck 
limit,
     consistent with a single poisoned segment buffer.
   - A short physical file cannot produce this exception at all: mapping a
     segment extends the file, so a truncated file yields zero-filled blocks (a
     parse error in `BPTreeNode`), never `newPosition > limit`.
   
   The window is small, but on a heavily loaded server (large UPDATEs, high heap
   pressure, weeks of uptime) it is eventually hit — and one hit permanently
   poisons the segment. The in-code comment acknowledges the assumption:
   `// Shouldn't (ha!) happen because the second "limit" resets`.
   
   ## Reproduction
   
   The trigger (an asynchronous throwable inside the 3-line window) cannot be
   provoked deterministically, but the state it leaves behind can be injected:
   shrink `segments[seg]`'s limit via reflection, then `read()` any block in 
that
   segment with a byte offset above the limit. This reproduces the exception
   verbatim, deterministically:
   
   ```
   java.lang.IllegalArgumentException: newPosition > limit: (448 > 64)
       at org.apache.jena.dboe.base.file.BlockAccessMapped.getByteBuffer(...)
   ```
   
   A test doing exactly this is included in the linked PR 
(`TestBlockAccessMappedSegmentState`).
   
   
   ### Relevant output and stacktrace
   
   ```shell
   [ERROR] 2026-07-03T09:40:00,154 [... | 
jena.dboe.base.file.BlockAccessMapped] Id: 218892
   [ERROR] ... Seg=213
   [ERROR] ... Segoff=6389760
   [ERROR] ... newPosition > limit: (6389760 > 2588672)
   java.lang.IllegalArgumentException: newPosition > limit: (6389760 > 2588672)
       at 
org.apache.jena.dboe.base.file.BlockAccessMapped.getByteBuffer(BlockAccessMapped.java:155)
       at 
org.apache.jena.dboe.base.file.BlockAccessMapped.read(BlockAccessMapped.java:93)
       ...
       at 
org.apache.jena.dboe.trans.bplustree.BPTreeNode.get(BPTreeNode.java:163)
   ```
   
   ### Are you interested in making a pull request?
   
   Yes


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to