Uh oh!
There was an error while loading. Please reload this page.
HDDS-15857. Fix checksum calculation in KeyValueHandler for variable-sized chunks - #10764
HDDS-15857. Fix checksum calculation in KeyValueHandler for variable-sized chunks#10764chungen0126 wants to merge 21 commits into
Conversation
chungen0126
commented
Jul 15, 2026
Hi @szetszwo , @sodonnel ,During my review of the current Stream Read architecture, I identified a checksum calculation issue that affects both the client and server sides.This issue occurs when a client actively triggers a flush or hsync. In such cases, the resulting chunk size does not perfectly align with
To properly verify checksums on the client side, I believe it is essential to return chunkInfo. If we go down this path, we should refine the structure of ReadBlockResponseProto, as the top-level checksumData might become redundant. Since modifying Protobuf definitions after a major release is highly problematic due to backward compatibility/wire-protocol locking, this issue could potentially block or impact the upcoming Ozone 2.2 release. I'd love to hear your thoughts on this. |
There was a problem hiding this comment.
Pull request overview
This PR fixes streaming-read checksum handling when block chunks are not uniform (variable-sized or smaller than bytesPerChunk) by making checksum selection and verification aware of real chunk boundaries, and by extending the read-block protocol to optionally return chunk metadata needed for correct verification.
Changes:
- Reworked server-side
KeyValueHandler#getChecksumsto select checksum slices based on chunk overlap rather than fixedbytesPerChunkindexing. - Extended
ReadBlockResponsePrototo optionally includechunkInfoList, and wired server/client to use chunk boundaries during checksum verification. - Added unit and integration tests covering varying/small chunk sizes with larger
bytesPerChecksum.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/client/rpc/read/TestStreamRead.java | Adds integration coverage for small chunks with larger checksums during streaming read. |
| hadoop-hdds/interface-client/src/main/proto/DatanodeClientProtocol.proto | Adds optional chunkInfoList to ReadBlockResponseProto for boundary-aware verification. |
| hadoop-hdds/container-service/src/test/java/org/apache/hadoop/ozone/container/keyvalue/TestKeyValueHandler.java | Adds unit tests validating checksum slicing with varying/small chunk sizes. |
| hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/keyvalue/KeyValueHandler.java | Updates checksum slicing logic and read-block response building to support variable chunk sizes. |
| hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/scm/protocolPB/ContainerCommandResponseBuilders.java | Populates chunk info into read-block responses (needs conditional sending for perf/compat). |
| hadoop-hdds/client/src/test/java/org/apache/hadoop/hdds/scm/storage/TestStreamBlockInputStream.java | Updates test response protos to include chunk info for checksum verification paths. |
| hadoop-hdds/client/src/main/java/org/apache/hadoop/hdds/scm/storage/StreamBlockInputStream.java | Adds boundary-aware checksum verification using chunkInfoList. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if (verifyChecksum) { | ||
| ChecksumData checksumData = ChecksumData.getFromProtoBuf(readBlock.getChecksumData()); | ||
| Checksum.verifyChecksum(data, checksumData, 0); | ||
| if (readBlock.hasChunkInfoList()) { | ||
| verifyChecksumForReadBlock(data, checksumData, readBlock); | ||
| } else { | ||
| throw new IOException("Checksum data is missing for block " + getBlockID()); | ||
| } |
| int bytesPerChecksum = checksumData.getBytesPerChecksum(); | ||
| long blockOffset = readBlock.getOffset(); | ||
| long readLength = data.remaining(); | ||
| long currentChunkOffset = 0; | ||
| int checksumIndex = 0; | ||
| int dataOffset = 0; | ||
| for (ContainerProtos.ChunkInfo chunk : readBlock.getChunkInfoList().getChunksList()) { | ||
| long chunkStart = currentChunkOffset; | ||
| long chunkEnd = chunkStart + chunk.getLen(); | ||
| long currentChunkOffset = 0; | ||
| for (ContainerProtos.ChunkInfo chunk : chunks) { | ||
| long chunkStart = currentChunkOffset; | ||
| long chunkEnd = chunkStart + chunk.getLen(); | ||
| long overlapStart = Math.max(blockOffset, chunkStart); | ||
| long overlapEnd = Math.min(blockOffset + readLength, chunkEnd); | ||
| public static ContainerCommandResponseProto getReadBlockResponse( | ||
| ContainerCommandRequestProto request, ChecksumData checksumData, ByteBuffer data, long offset) { | ||
| ContainerCommandRequestProto request, ChecksumData checksumData, | ||
| ByteBuffer data, long offset, List<ChunkInfo> chunkInfoList, boolean verifyChecksum) { | ||
| ContainerProtos.ReadBlockResponseProto response = ContainerProtos.ReadBlockResponseProto.newBuilder() | ||
| ContainerProtos.ReadBlockResponseProto response = ReadBlockResponseProto.newBuilder() | ||
| .setChecksumData(checksumData.getProtoBufMessage()) | ||
| .setData(ByteString.copyFrom(data)) | ||
| .setOffset(offset) | ||
| .setChunkInfoList(ChunkInfoList.newBuilder().addAllChunks(chunkInfoList).build()) | ||
| .build(); |
There was a problem hiding this comment.
good catch, should be fixed
There was a problem hiding this comment.
The response size concern is valid, but verifyChecksum would not work here. In prod, readBlock() always calls readBlockImpl(..., false) because that param controls datanode side verification, whereas client-side checksum verification is enabled independently. Gating chunkInfoList on this parameter would therefore omit the metadata from production responses.
We need either an explicit request capability or only the chunk metadata overlapping each response, with the client using absolute ChunkInfo.offset values.
| OzoneConfiguration conf = cluster.getConf(); | ||
| OzoneClientConfig clientConfig = conf.getObject(OzoneClientConfig.class); | ||
| clientConfig.setStreamReadBlock(true); | ||
| clientConfig.setStreamBufferFlushDelay(false); | ||
| final OzoneConfiguration steamReadConf = new OzoneConfiguration(conf); | ||
| steamReadConf.setFromObject(clientConfig); | ||
| try (OzoneClient streamReadClient = OzoneClientFactory.getRpcClient(steamReadConf)) { |
jojochuang
commented
Jul 15, 2026
Bugbot reviewStream checksum verify misaligns chunks — With Generated-by: Cursor Bugbot |
jojochuang
commented
Jul 20, 2026
This PR has been marked as stale due to 21 days of inactivity. Please comment or remove the stale label to keep it open. Otherwise, it will be automatically closed in 7 days. |
yandrey321
commented
Aug 11, 2026
what is the line/branch coverage for the new code? Are there test that covers Checksum = None? |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
| Checksum.verifyChecksum(data, checksumData, 0); | ||
| if (readBlock.hasChunkInfoList()) { | ||
| verifyChecksumForReadBlock(data, checksumData, readBlock); | ||
| } else { |
There was a problem hiding this comment.
optional chunkInfoList keeps the wire compatible, but throwing when it's absent is not upgrade-compatible: a new client reading from an old datanode gets a hard failure on every streaming read. Please fall back to the previous verification path (or gate on datanode version) when chunkInfoList is missing.
There was a problem hiding this comment.
Good point. I will update the code to fall back to the legacy checksum verification path (Checksum.verifyChecksum(data, checksumData, 0)) when readBlock.hasChunkInfoList() is false.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
yandrey321
left a comment
There was a problem hiding this comment.
Please see comments above
Thanks @chungen0126 for the patch. It would also be a good idea to rebase the PR branch (merge master into this) since it is 200+ commits behind at this point. |
Thanks @yandrey321 for pointing this out. The core issue is that the initial Since each chunk maintains its own checksum grid starting from its byte 0, the read alignment only needs to be adjusted for the starting offset relative to that chunk. Once the start offset is correctly aligned, the subsequent chunk processing and iteration logic remain valid. I will update the initial offset calculation accordingly. |
szetszwo
commented
Aug 14, 2026
@chungen0126 , could you review #10415 first? It has be around for a long time. We should get it merged. |
szetszwo
commented
Aug 17, 2026
@chungen0126 , thanks for reviewing #10415 ! After merging it, this probably need to be updated. Please let me know when this is ready to be reviewed. |
chungen0126
commented
Aug 18, 2026
@szetszwo Updated! Please take a look when you are available. Thanks! |
szetszwo
left a comment
There was a problem hiding this comment.
@chungen0126 , thanks for working on this!
This change is quite complicated. Let's fix the server first and separate the client change in a separated JIRA.
See the comments inlined and also https://issues.apache.org/jira/secure/attachment/13084032/10764_review.patch
| } | ||
| currentChunkOffset = chunkEnd; | ||
| } | ||
| return blockOffset; |
There was a problem hiding this comment.
This is out-of-range case is not supposed to happen. So, it should throw an exception:
thrownewIllegalStateException("blockOffset " + blockOffset + " is out of bounds: " + chunkInfos);| if (checksumType != ContainerProtos.ChecksumType.NONE) { | ||
| final long chunkRelativeOffset = getChunkRelativeOffset(readBlock.getOffset(), chunkInfos); | ||
| offsetAlignment = chunkRelativeOffset % bytesPerChecksum; | ||
| } else { | ||
| offsetAlignment = readBlock.getOffset() % bytesPerChecksum; | ||
| } |
There was a problem hiding this comment.
Even for ChecksumType.NONE, it should use getChunkRelativeOffset(..).
finallongoffsetAlignment = getChunkRelativeOffset(readBlock.getOffset(), chunkInfos) % bytesPerChecksum;
longadjustedOffset = readBlock.getOffset() - offsetAlignment;| final List<ByteString> checksums = getChecksums(adjustedOffset, readLength, | ||
| bytesPerChunk, bytesPerChecksum, chunkInfos); | ||
| bytesPerChecksum, chunkInfos); | ||
| LOG.debug("Read {} at adjustedOffset {}, readLength {}, bytesPerChunk {}, bytesPerChecksum {}", | ||
| readBlock, adjustedOffset, readLength, bytesPerChunk, bytesPerChecksum); |
There was a problem hiding this comment.
Make it a single line and remove bytesPerChunk.
@@ -2355,7 +2355,6 @@ private long readBlockImpl(ContainerCommandRequestProto request, RandomAccessFil
return 0;
}
final List<ContainerProtos.ChunkInfo> chunkInfos = blockData.getChunks();
- final int bytesPerChunk = Math.toIntExact(chunkInfos.get(0).getLen());
final ChecksumType checksumType = chunkInfos.get(0).getChecksumData().getType();
ChecksumData checksumData = null;
int bytesPerChecksum = STREAMING_BYTES_PER_CHUNK;finalList<ByteString> checksums = getChecksums(adjustedOffset, readLength, bytesPerChecksum, chunkInfos);
LOG.debug("Read {} at adjustedOffset {}, readLength {}, bytesPerChecksum {}",
readBlock, adjustedOffset, readLength, bytesPerChecksum);| public static void verifyChecksum( | ||
| ByteBuffer data, ChecksumData checksumData, long blockOffset, List<ContainerProtos.ChunkInfo> chunkInfoList) | ||
| throws OzoneChecksumException { | ||
| if (!checksumData.getChecksumType().equals(ChecksumType.NONE)) { | ||
| int bytesPerChecksum = checksumData.getBytesPerChecksum(); | ||
| long readLength = data.remaining(); | ||
| long currentChunkOffset = 0; | ||
| int checksumIndex = 0; | ||
| int dataOffset = 0; | ||
| for (ContainerProtos.ChunkInfo chunk : chunkInfoList) { | ||
| long chunkStart = currentChunkOffset; | ||
| long chunkEnd = chunkStart + chunk.getLen(); | ||
| long overlapStart = Math.max(blockOffset, chunkStart); | ||
| long overlapEnd = Math.min(blockOffset + readLength, chunkEnd); | ||
| if (overlapStart < overlapEnd) { | ||
| int overlapLen = Math.toIntExact(overlapEnd - overlapStart); | ||
| ByteBuffer chunkData = data.duplicate(); | ||
| chunkData.position(data.position() + dataOffset); | ||
| chunkData.limit(data.position() + dataOffset + overlapLen); | ||
| Checksum.verifyChecksum(chunkData, checksumData, checksumIndex); | ||
| dataOffset += overlapLen; | ||
| long offsetInChunk = overlapStart - chunkStart; | ||
| long endOffsetInChunk = overlapEnd - chunkStart; | ||
| int firstChecksumIndex = Math.toIntExact(offsetInChunk / bytesPerChecksum); | ||
| int lastChecksumIndex = Math.toIntExact((endOffsetInChunk - 1) / bytesPerChecksum); | ||
| checksumIndex += (lastChecksumIndex - firstChecksumIndex + 1); | ||
| } | ||
| currentChunkOffset += chunk.getLen(); | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Questions: Is it possible to have chunk length not a multiple of bytesPerChecksum?
If yes, suppose
- bytesPerChecksum is 16, and
- chunkList(offset, length): (0, 10), (10, 20), (30, 10)
Then, how many checksums does it need? Is it 4 (= 1 + 2 + 1)?
Comments on the code:
- We should use binary search to find the startIndex
- Assert chunkList
- We should add some tests for this method.
- The calculation can be simplified:
staticvoidassertChunkInfos(List<ChunkInfo> chunkInfos, longblockOffset, intstartIndex) {
longpreviousChunkEnd = -1;
for (inti = 0; i < chunkInfos.size(); i++) {
finalChunkInfochunk = chunkInfos.get(i);
if (previousChunkEnd >= 0) {
Preconditions.assertSame(previousChunkEnd, chunk.getOffset(), "chunkOffset");
}
Preconditions.assertTrue(chunk.getLen() > 0);
finallongchunkEnd = chunk.getOffset() + chunk.getLen();
if (i < startIndex) {
Preconditions.assertTrue(blockOffset >= chunkEnd);
} elseif (i == startIndex) {
Preconditions.assertTrue(blockOffset >= chunk.getOffset());
Preconditions.assertTrue(blockOffset < chunkEnd);
} else {
Preconditions.assertTrue(blockOffset < chunk.getOffset());
}
previousChunkEnd = chunkEnd;
}
}
publicstaticvoidverifyChecksum(ByteBufferdata, ChecksumDatachecksumData, longblockOffset, List<ChunkInfo> chunkInfoList)
throwsOzoneChecksumException {
if (checksumData.getChecksumType() == ChecksumType.NONE) {
return;
}
finalintbytesPerChecksum = checksumData.getBytesPerChecksum();
finallongreadEnd = blockOffset + data.remaining();
finalintsearchIndex = Collections.binarySearch(chunkInfoList,
ChunkInfo.newBuilder().setOffset(blockOffset).build(),
Comparator.comparing(ChunkInfo::getOffset));
finalintstartIndex = searchIndex >= 0 ? searchIndex : -(searchIndex + 1);
assertChunkInfos(chunkInfoList, blockOffset, startIndex);
for (inti = startIndex; i < chunkInfoList.size(); i++) {
finalChunkInfochunk = chunkInfoList.get(i);
if (readEnd <= chunk.getOffset()) {
return;
}
finalintdataOffset = i == startIndex ? 0 : Math.toIntExact(chunk.getOffset() - blockOffset);
finallongchunkEnd = chunk.getOffset() + chunk.getLen();
finalintdataEnd = Math.toIntExact(Math.min(chunkEnd, readEnd) - blockOffset);
finalByteBufferchunkData = data.duplicate();
chunkData.position(data.position() + dataOffset);
chunkData.limit(data.position() + dataEnd);
// TODO: check the calculation of checksumIndex below finalintchecksumIndex = Math.toIntExact(chunk.getOffset() / bytesPerChecksum);
verifyChecksum(chunkData, checksumData, checksumIndex);
}
}
What changes were proposed in this pull request?
Problem
Currently, the
getChecksumsmethod inKeyValueHandler.javafails or returns incorrect checksums when chunks have variable sizes or are smaller than bytesPerChunk.The original code assumes all chunks (except the last one) have a fixed size equal to
bytesPerChunk. This assumption is incorrect when chunks have different lengths, leading to wrong index calculations.Crucially, when checksum verification is enabled, the DataNode must return the actual ChunkInfo (which contains chunk lengths) to the client. Without this metadata, the client cannot perceive the boundaries of each varying chunk, making it impossible to correctly map the offsets and compute/verify the checksums.
Solution
This PR resolves the issue by tracking exact chunk boundaries on both the server and client sides:
Boundary-Aware Verification: In StreamBlockInputStream#onNext, if the response contains chunkInfoList and checksum verification is enabled:
What is the link to the Apache JIRA
https://issues.apache.org/jira/browse/HDDS-15857
How was this patch tested?
Added
testGetChecksumsWithVaryingChunkSizesandtestGetChecksumsWithSmallChunksinTestKeyValueHandler.java.Added
testSmallChunksWithLargeChecksuminTestStreamRead.java.CI: https://github.com/chungen0126/ozone/actions/runs/29381590591