You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
In a BookKeeper 4.16.7 based deployment using DbLedgerStorage and DefaultEntryLogger, we observed an entrylog where the entry bytes are still physically present and mostly parseable, but both the RocksDB location index and the entrylog header ledgersMapOffset point to positions that are consistently too early by the same offset.
The suspected code-level issue is in BufferedChannel. BufferedChannel.write() can call flush() before updating its logical position. If flush() advances the underlying FileChannel position and then throws IOException, the physical file position has moved forward but BufferedChannel.position has not. The channel is not marked failed, so later writes can continue using the same stale logical position.
Relevant code shape:
// BufferedChannel.write(...)writeBuffer.writeBytes(...);
if (!writeBuffer.isWritable()) {
flush();
}
position += copied;
// BufferedChannel.flush(...)ByteBuffertoWrite = writeBuffer.internalNioBuffer(0, writeBuffer.writerIndex());
do {
fileChannel.write(toWrite);
} while (toWrite.hasRemaining());
writeBuffer.clear();
writeBufferStartPosition.set(fileChannel.position());
If fileChannel.write(toWrite) performs a partial local write and then throws, writeBuffer.clear() and writeBufferStartPosition.set(...) are skipped, and the caller in write() also skips position += copied.
This can affect the main entrylog writer in two places:
// EntryLogManagerBase.addEntry(...)logChannel.write(sizeBuffer);
longpos = logChannel.position(); // used as the entry body locationlogChannel.write(entry);
return (logChannel.getLogId() << 32L) | pos;
So the same stale logical position can be published as:
entry locations in RocksDB, and
the entrylog header ledgersMapOffset.
This makes acknowledged entries unreadable even though their bytes may still exist later in the same entrylog file.
To Reproduce
A full production reproduction is hard because it requires a partial local write followed by an IOException at a specific point. However, the bad state is reachable with a small fault-injected FileChannel.
A reproducer-only PR is available to demonstrate the issue on the BookKeeper test path:
mvn -pl bookkeeper-server -Dtest=org.apache.bookkeeper.bookie.BufferedChannelTest test
The reproducer injects a FileChannel that writes part of the buffer, advances its physical position, then throws IOException during BufferedChannel.flush().
testPositionCanLagFileChannelAfterPartialFlushFailure shows that after the exception, FileChannel.position() can be ahead of BufferedChannel.position(), and a later write can still succeed on the same channel.
testLedgersMapHeaderUsesStalePositionAfterPartialFlushFailure shows that BufferedLogChannel.appendLedgersMap() can write a stale ledgersMapOffset to the entrylog header after that drift.
The tests currently assert the reachable bad state. After a fix, these tests should be rewritten to assert fail-closed behavior instead.
Expected behavior
After an IOException from a write path where the local writer state is no longer trustworthy, BookKeeper should fail closed instead of continuing to reuse the same entrylog writer.
At minimum:
BufferedChannel should enter a failed state after write(), flush(), or forceWrite() fails with IOException.
A failed BufferedChannel should reject subsequent write(), flush(), forceWrite(), and appendLedgersMap() attempts.
EntryLogger.addEntry() should not return a location after the underlying log channel has failed.
appendLedgersMap() should not publish a header pointing to a stale logical position.
The direct positioned header write in appendLedgersMap() should use full-write semantics and failure handling, because FileChannel.write(ByteBuffer, position) is not guaranteed to write all bytes in one call.
The main entrylog writer failure should be propagated out of DbLedgerStorage / SyncThread instead of being only logged and then continuing to accept writes.
For the main entrylog writer, this likely needs fatal/fail-stop handling for the affected bookie process. Continuing to accept writes can persist stale entry locations into RocksDB. Compaction entrylog failures may need a different policy: abort compaction and keep the source entrylog, rather than immediately shutting down the bookie.
The same delta appears independently in the ledgers map header and in RocksDB entry locations:
delta = 34553
header.ledgersMapOffset = 1073741388
real ledgers map start = 1073775941
delta = 34553
L15069 actual body position - RocksDB indexed position = 34553
The delta also matches the 64 KiB write-buffer boundary at the corruption point:
569 / 569 L15069 entries pass BK V3 + CRC32C validation after +34553.
sum(4 + size) for L15069 = 26164.
The real ledgers map records L15069 TotalSize = 26164.
The real ledgers map is also internally consistent with the file size:
Two independent verification passes checked the same evidence:
One pass independently confirmed the big-endian header parsing, real ledgers map offset, L15069 CRC32C validation, and sum(4 + size) = 26164.
Another pass independently mmap-scanned the file and confirmed 65536..1073775941 is a continuous physical entry chain, with delta_buffer = delta_map = 34553.
This issue is about the BookKeeper write-path failure mode and fail-closed behavior. It is not a request for corrupted-data repair tooling.
BUG REPORT
Describe the bug
In a BookKeeper 4.16.7 based deployment using
DbLedgerStorageandDefaultEntryLogger, we observed an entrylog where the entry bytes are still physically present and mostly parseable, but both the RocksDB location index and the entrylog headerledgersMapOffsetpoint to positions that are consistently too early by the same offset.The suspected code-level issue is in
BufferedChannel.BufferedChannel.write()can callflush()before updating its logicalposition. Ifflush()advances the underlyingFileChannelposition and then throwsIOException, the physical file position has moved forward butBufferedChannel.positionhas not. The channel is not marked failed, so later writes can continue using the same stale logical position.Relevant code shape:
If
fileChannel.write(toWrite)performs a partial local write and then throws,writeBuffer.clear()andwriteBufferStartPosition.set(...)are skipped, and the caller inwrite()also skipsposition += copied.This can affect the main entrylog writer in two places:
So the same stale logical position can be published as:
ledgersMapOffset.This makes acknowledged entries unreadable even though their bytes may still exist later in the same entrylog file.
To Reproduce
A full production reproduction is hard because it requires a partial local write followed by an
IOExceptionat a specific point. However, the bad state is reachable with a small fault-injectedFileChannel.A reproducer-only PR is available to demonstrate the issue on the BookKeeper test path:
Steps:
branch-4.16.mvn -pl bookkeeper-server -Dtest=org.apache.bookkeeper.bookie.BufferedChannelTest testFileChannelthat writes part of the buffer, advances its physical position, then throwsIOExceptionduringBufferedChannel.flush().testPositionCanLagFileChannelAfterPartialFlushFailureshows that after the exception,FileChannel.position()can be ahead ofBufferedChannel.position(), and a later write can still succeed on the same channel.testLedgersMapHeaderUsesStalePositionAfterPartialFlushFailureshows thatBufferedLogChannel.appendLedgersMap()can write a staleledgersMapOffsetto the entrylog header after that drift.The tests currently assert the reachable bad state. After a fix, these tests should be rewritten to assert fail-closed behavior instead.
Expected behavior
After an
IOExceptionfrom a write path where the local writer state is no longer trustworthy, BookKeeper should fail closed instead of continuing to reuse the same entrylog writer.At minimum:
BufferedChannelshould enter a failed state afterwrite(),flush(), orforceWrite()fails withIOException.BufferedChannelshould reject subsequentwrite(),flush(),forceWrite(), andappendLedgersMap()attempts.EntryLogger.addEntry()should not return a location after the underlying log channel has failed.appendLedgersMap()should not publish a header pointing to a stale logical position.appendLedgersMap()should use full-write semantics and failure handling, becauseFileChannel.write(ByteBuffer, position)is not guaranteed to write all bytes in one call.DbLedgerStorage/SyncThreadinstead of being only logged and then continuing to accept writes.For the main entrylog writer, this likely needs fatal/fail-stop handling for the affected bookie process. Continuing to accept writes can persist stale entry locations into RocksDB. Compaction entrylog failures may need a different policy: abort compaction and keep the source entrylog, rather than immediately shutting down the bookie.
Screenshots
Physical vs logical position drift diagram:
Additional context
Production evidence from the corrupted entrylog:
The same delta appears independently in the ledgers map header and in RocksDB entry locations:
The delta also matches the 64 KiB write-buffer boundary at the corruption point:
Physical entry parsing shows that the file becomes a continuous valid physical entry chain again at the 64 KiB boundary:
Independent scan result:
For ledger
15069, every RocksDB location in entrylog2133becomes valid after adding the same delta:Samples:
Additional validation:
The real ledgers map is also internally consistent with the file size:
Two independent verification passes checked the same evidence:
sum(4 + size) = 26164.65536..1073775941is a continuous physical entry chain, withdelta_buffer = delta_map = 34553.This issue is about the BookKeeper write-path failure mode and fail-closed behavior. It is not a request for corrupted-data repair tooling.