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]
