This is an automated email from the ASF dual-hosted git repository.
szetszwo pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/ozone.git
The following commit(s) were added to refs/heads/master by this push:
new d3843b157b1 HDDS-15857. Fix checksum calculation in KeyValueHandler
for variable-sized chunks (#10764)
d3843b157b1 is described below
commit d3843b157b15094c10154f429ce33f260b23209d
Author: Chung En Lee <[email protected]>
AuthorDate: Tue Sep 15 13:05:26 2026 +0800
HDDS-15857. Fix checksum calculation in KeyValueHandler for variable-sized
chunks (#10764)
---
.../org/apache/hadoop/ozone/common/Checksum.java | 56 ++++++
.../container/common/impl/HddsDispatcher.java | 2 +-
.../ozone/container/common/interfaces/Handler.java | 2 +-
.../ozone/container/keyvalue/KeyValueHandler.java | 67 ++++---
.../container/keyvalue/ReadBlockComputation.java | 157 ++++++++++++++++
.../container/keyvalue/TestKeyValueHandler.java | 199 ++++++++++++++++++++-
6 files changed, 450 insertions(+), 33 deletions(-)
diff --git
a/hadoop-hdds/common/src/main/java/org/apache/hadoop/ozone/common/Checksum.java
b/hadoop-hdds/common/src/main/java/org/apache/hadoop/ozone/common/Checksum.java
index aefe4399a46..128666784eb 100644
---
a/hadoop-hdds/common/src/main/java/org/apache/hadoop/ozone/common/Checksum.java
+++
b/hadoop-hdds/common/src/main/java/org/apache/hadoop/ozone/common/Checksum.java
@@ -17,6 +17,8 @@
package org.apache.hadoop.ozone.common;
+import static org.apache.ratis.util.Preconditions.assertSame;
+
import com.google.common.annotations.VisibleForTesting;
import java.nio.ByteBuffer;
import java.security.MessageDigest;
@@ -27,6 +29,7 @@
import java.util.function.Supplier;
import org.apache.hadoop.hdds.protocol.datanode.proto.ContainerProtos;
import
org.apache.hadoop.hdds.protocol.datanode.proto.ContainerProtos.ChecksumType;
+import
org.apache.hadoop.hdds.protocol.datanode.proto.ContainerProtos.ChunkInfo;
import org.apache.hadoop.hdds.utils.db.IntegerCodec;
import org.apache.hadoop.ozone.common.utils.BufferUtils;
import org.apache.ratis.thirdparty.com.google.protobuf.ByteString;
@@ -435,6 +438,59 @@ public static void verifyChecksum(List<ByteBuffer>
bufferList, int startIndex, C
checksumData.verifyChecksumDataMatches(startIndex, computed);
}
+ public static void validateChecksums(ByteBuffer data, long blockOffset, int
startIndex,
+ final List<ChunkInfo> chunks) throws OzoneChecksumException {
+
+ long firstChunkOffset = blockOffset - chunks.get(startIndex).getOffset();
+ int bytesPerChecksum =
chunks.get(startIndex).getChecksumData().getBytesPerChecksum();
+ long readLength = data.remaining();
+ int dataOffset = data.position();
+
+ if (readLength <= 0) {
+ return;
+ }
+
+ assertSame(0, firstChunkOffset % bytesPerChecksum, "blockOffset %
bytesPerChecksum");
+ ContainerProtos.ChunkInfo firstChunk = chunks.get(startIndex);
+
+ // verify first chunk, only the first chunk is not start at zero.
+ int firstChunkIndex = (int) (firstChunkOffset / bytesPerChecksum);
+ int dataLimit = (int) Math.min(firstChunk.getLen() - firstChunkOffset,
readLength);
+ Checksum.verifySingleChunk(data, dataOffset, dataLimit, firstChunk,
firstChunkIndex);
+ dataOffset += dataLimit;
+ readLength -= dataLimit;
+ startIndex++;
+
+ while (readLength > 0) {
+ ContainerProtos.ChunkInfo chunkInfo = chunks.get(startIndex);
+ dataLimit = (int) Math.min(chunkInfo.getLen(), readLength);
+
+ Checksum.verifySingleChunk(data, dataOffset, dataLimit, chunkInfo, 0);
+
+ dataOffset += dataLimit;
+ readLength -= dataLimit;
+ startIndex++;
+ }
+
+ }
+
+ private static void verifySingleChunk(ByteBuffer data, int dataOffset, int
dataLimit,
+ ContainerProtos.ChunkInfo chunkInfo, int checksumIndex) throws
OzoneChecksumException {
+
+ ContainerProtos.ChecksumData protoChecksum = chunkInfo.getChecksumData();
+ final ByteBuffer buffer = data.duplicate();
+ buffer.position(dataOffset);
+ buffer.limit(dataOffset + dataLimit);
+
+ final ChecksumData checksum = new ChecksumData(
+ protoChecksum.getType(),
+ protoChecksum.getBytesPerChecksum(),
+ protoChecksum.getChecksumsList()
+ );
+
+ Checksum.verifyChecksum(buffer, checksum, checksumIndex);
+ }
+
/**
* Returns a ChecksumData with type NONE for testing.
*/
diff --git
a/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/common/impl/HddsDispatcher.java
b/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/common/impl/HddsDispatcher.java
index 1564e3ddbf5..34298c7263f 100644
---
a/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/common/impl/HddsDispatcher.java
+++
b/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/common/impl/HddsDispatcher.java
@@ -877,7 +877,7 @@ public void streamDataReadOnly(ContainerCommandRequestProto
msg,
ContainerProtos.Result.CONTAINER_INTERNAL_ERROR);
}
perf.appendPreOpLatencyMs(Time.monotonicNow() - startTime);
- responseProto = handler.readBlock(msg, container, blockFile,
streamObserver);
+ responseProto = handler.readBlock(msg, container, blockFile,
streamObserver, false);
long oPLatencyMS = Time.monotonicNow() - startTime;
metrics.incContainerOpsLatencies(cmdType, oPLatencyMS);
if (responseProto == null) {
diff --git
a/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/common/interfaces/Handler.java
b/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/common/interfaces/Handler.java
index d6828423a36..53dd03627e1 100644
---
a/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/common/interfaces/Handler.java
+++
b/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/common/interfaces/Handler.java
@@ -302,5 +302,5 @@ public abstract void copyContainer(
public abstract ContainerCommandResponseProto readBlock(
ContainerCommandRequestProto msg, Container container,
RandomAccessFileChannel blockFile,
- StreamObserver<ContainerCommandResponseProto> streamObserver);
+ StreamObserver<ContainerCommandResponseProto> streamObserver, boolean
testVariableChunks);
}
diff --git
a/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/keyvalue/KeyValueHandler.java
b/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/keyvalue/KeyValueHandler.java
index d872d7a91e2..8dd68427001 100644
---
a/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/keyvalue/KeyValueHandler.java
+++
b/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/keyvalue/KeyValueHandler.java
@@ -57,7 +57,6 @@
import static
org.apache.hadoop.hdds.scm.protocolPB.ContainerCommandResponseBuilders.putBlockResponseSuccess;
import static
org.apache.hadoop.hdds.scm.protocolPB.ContainerCommandResponseBuilders.unsupportedRequest;
import static
org.apache.hadoop.hdds.scm.utils.ClientCommandsUtils.getReadChunkVersion;
-import static org.apache.hadoop.hdds.utils.IOUtils.roundUp;
import static org.apache.hadoop.ozone.OzoneConsts.INCREMENTAL_CHUNK_LIST;
import static
org.apache.hadoop.ozone.container.checksum.DNContainerOperationClient.createSingleNodePipeline;
import static
org.apache.hadoop.ozone.container.common.impl.ContainerLayoutVersion.DEFAULT_LAYOUT;
@@ -2285,7 +2284,7 @@ boolean deleteUnreferencedFile(File file) {
public ContainerCommandResponseProto readBlock(
ContainerCommandRequestProto request, Container kvContainer,
RandomAccessFileChannel blockFile,
- StreamObserver<ContainerCommandResponseProto> streamObserver) {
+ StreamObserver<ContainerCommandResponseProto> streamObserver, boolean
testVariableChunks) {
if (kvContainer.getContainerData().getLayoutVersion() != FILE_PER_BLOCK) {
return ContainerUtils.logAndReturnError(LOG,
@@ -2301,7 +2300,7 @@ public ContainerCommandResponseProto readBlock(
}
try {
final long startTime = Time.monotonicNow();
- final long bytesRead = readBlockImpl(request, blockFile, kvContainer,
streamObserver, false);
+ final long bytesRead = readBlockImpl(request, blockFile, kvContainer,
streamObserver, testVariableChunks);
KeyValueContainerData containerData = (KeyValueContainerData) kvContainer
.getContainerData();
HddsVolume volume = containerData.getVolume();
@@ -2325,7 +2324,7 @@ public ContainerCommandResponseProto readBlock(
}
private long readBlockImpl(ContainerCommandRequestProto request,
RandomAccessFileChannel blockFile,
- Container kvContainer, StreamObserver<ContainerCommandResponseProto>
streamObserver, boolean verifyChecksum)
+ Container kvContainer, StreamObserver<ContainerCommandResponseProto>
streamObserver, boolean testVariableChunks)
throws IOException {
final ReadBlockRequestProto readBlock = request.getReadBlock();
int responseDataSize = readBlock.getResponseDataSize();
@@ -2355,32 +2354,36 @@ 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;
- if (checksumType == ContainerProtos.ChecksumType.NONE) {
- checksumData = new ChecksumData(checksumType, 0);
- } else {
+ if (checksumType != ContainerProtos.ChecksumType.NONE) {
bytesPerChecksum =
chunkInfos.get(0).getChecksumData().getBytesPerChecksum();
}
- // We have to align the read to checksum boundaries, so whatever offset is
requested, we have to move back to the
- // previous checksum boundary.
- // eg if bytesPerChecksum is 512, and the requested offset is 600, we have
to move back to 512.
- // If the checksum type is NONE, we don't have to do this, but using no
checksums should be rare in practice and
- // it simplifies the code to always do this.
- final long offsetAlignment = readBlock.getOffset() % bytesPerChecksum;
- long adjustedOffset = readBlock.getOffset() - offsetAlignment;
+ // TODO: Support client-side flag to toggle checksum verification.
+ // If checksum is disabled, chunk offset adjustment can be skipped.
+ int chunkIndex = ReadBlockComputation.searchChunk(readBlock.getOffset(),
chunkInfos);
+ ReadBlockComputation readBlockComputation =
+ new ReadBlockComputation(responseDataSize, bytesPerChecksum,
chunkInfos, chunkIndex);
+ long adjustedOffset =
readBlockComputation.computeAdjustedOffset(readBlock.getOffset());
+
+ long adjustLength = readBlockComputation.computeAdjustedLength(
+ readBlock.getOffset(), readBlock.getLength(), adjustedOffset);
+
+ ChecksumData checksumData = new ChecksumData(checksumType,
bytesPerChecksum);
final ByteBuffer buffer = ByteBuffer.allocate(responseDataSize);
blockFile.position(adjustedOffset);
long totalDataLength = 0;
int numResponses = 0;
- final long rounded = roundUp(readBlock.getLength() + offsetAlignment,
bytesPerChecksum);
- final long requiredLength = Math.min(rounded, blockData.getSize() -
adjustedOffset);
+ Preconditions.checkState(adjustLength <= blockData.getSize() -
adjustedOffset);
LOG.debug("adjustedOffset {}, requiredLength {}, blockSize {}",
- adjustedOffset, requiredLength, blockData.getSize());
- for (boolean shouldRead = true; totalDataLength < requiredLength &&
shouldRead;) {
+ adjustedOffset, adjustLength, blockData.getSize());
+ for (boolean shouldRead = true; totalDataLength < adjustLength &&
shouldRead;) {
+
+ int bufferLimit =
readBlockComputation.computeBufferLimit(adjustedOffset, adjustLength -
totalDataLength);
+
+ buffer.limit(bufferLimit);
+
shouldRead = blockFile.read(buffer);
buffer.flip();
final int readLength = buffer.remaining();
@@ -2391,14 +2394,20 @@ private long readBlockImpl(ContainerCommandRequestProto
request, RandomAccessFil
assertTrue(readLength > 0, () -> "readLength = " + readLength + " <= 0");
if (checksumType != ContainerProtos.ChecksumType.NONE) {
- final List<ByteString> checksums = getChecksums(adjustedOffset,
readLength,
- bytesPerChunk, bytesPerChecksum, chunkInfos);
- LOG.debug("Read {} at adjustedOffset {}, readLength {}, bytesPerChunk
{}, bytesPerChecksum {}",
- readBlock, adjustedOffset, readLength, bytesPerChunk,
bytesPerChecksum);
- checksumData = new ChecksumData(checksumType, bytesPerChecksum,
checksums);
- if (verifyChecksum) {
- Checksum.verifyChecksum(buffer.duplicate(), checksumData, 0);
+
+ if (!testVariableChunks) {
+ // This is the old approach.
+ // It has some bugs, but we are keeping it for now due to backward
compatibility.
+ checksumData = new ChecksumData(checksumType, bytesPerChecksum,
getChecksums(adjustedOffset, readLength,
+ bytesPerChecksum, chunkInfos));
+ }
+
+
+ if (validateChunkChecksumData) {
+ Checksum.validateChecksums(buffer.duplicate(), adjustedOffset,
chunkIndex, chunkInfos);
}
+ LOG.debug("Read {} at adjustedOffset {}, readLength {},
bytesPerChecksum {}",
+ readBlock, adjustedOffset, readLength, bytesPerChecksum);
}
final ContainerCommandResponseProto response = getReadBlockResponse(
request, checksumData, buffer, adjustedOffset);
@@ -2411,12 +2420,14 @@ private long readBlockImpl(ContainerCommandRequestProto
request, RandomAccessFil
adjustedOffset += readLength;
totalDataLength += dataLength;
numResponses++;
+ chunkIndex = readBlockComputation.findChunk(adjustedOffset);
}
return totalDataLength;
}
- static List<ByteString> getChecksums(long blockOffset, int readLength, int
bytesPerChunk, int bytesPerChecksum,
+ static List<ByteString> getChecksums(long blockOffset, int readLength, int
bytesPerChecksum,
final List<ContainerProtos.ChunkInfo> chunks) {
+ final int bytesPerChunk = Math.toIntExact(chunks.get(0).getLen());
assertSame(0, blockOffset % bytesPerChecksum, "blockOffset %
bytesPerChecksum");
final int numChecksums = 1 + (readLength - 1) / bytesPerChecksum;
final List<ByteString> checksums = new ArrayList<>(numChecksums);
diff --git
a/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/keyvalue/ReadBlockComputation.java
b/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/keyvalue/ReadBlockComputation.java
new file mode 100644
index 00000000000..b08e9062ac0
--- /dev/null
+++
b/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/keyvalue/ReadBlockComputation.java
@@ -0,0 +1,157 @@
+/*
+ * 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.hadoop.ozone.container.keyvalue;
+
+import java.util.List;
+import
org.apache.hadoop.hdds.protocol.datanode.proto.ContainerProtos.ChunkInfo;
+import org.apache.ratis.util.Preconditions;
+
+/**
+ * Utility class to compute checksum-aligned offsets, lengths, and buffer
+ * limits for streaming readBlock operations.
+ */
+class ReadBlockComputation {
+
+ private final int responseDataSize;
+ private final int bitMask;
+ private final int bytesPerChecksum;
+ private final List<ChunkInfo> chunks;
+ private int chunkIndex;
+ private long lastPosition;
+
+ ReadBlockComputation(int responseDataSize, int bytesPerChecksum,
+ List<ChunkInfo> chunks, int firstChunkIndex) {
+ this.responseDataSize = responseDataSize;
+
+ Preconditions.assertSame(1, Long.bitCount(bytesPerChecksum), "bitCount");
// check power of 2.
+ // Suppose bytesPerChecksum = 00001000 (a power of 2), then bitMask =
11111000.
+ // The following two computations are the same:
+ // We will use (n & bitMask) to compute ((n / bytesPerChecksum) *
bytesPerChecksum).
+ this.bitMask = -bytesPerChecksum;
+ this.bytesPerChecksum = bytesPerChecksum;
+
+ this.chunks = chunks;
+ this.chunkIndex = firstChunkIndex;
+ this.lastPosition = 0;
+ }
+
+ /**
+ * Binary-search for the index of the chunk whose start offset is the
+ * largest value ≤ {@code targetOffset}.
+ */
+ static int searchChunk(long targetOffset, List<ChunkInfo> chunkInfoList) {
+ int low = 0;
+ int high = chunkInfoList.size() - 1;
+ while (low <= high) {
+ int mid = (low + high) >>> 1;
+ long midVal = chunkInfoList.get(mid).getOffset();
+ if (midVal <= targetOffset) {
+ low = mid + 1;
+ } else {
+ high = mid - 1;
+ }
+ }
+ return high;
+ }
+
+ /**
+ * Return the read offset aligned back to the previous checksum boundary
+ * within the chunk that contains {@code blockOffset}.
+ */
+ long computeAdjustedOffset(long blockOffset) {
+ long offsetAlignment = (blockOffset - chunks.get(chunkIndex).getOffset())
% bytesPerChecksum;
+ return blockOffset - offsetAlignment;
+ }
+
+ /**
+ * Return the length of data that must be read so that the range
+ * {@code [blockOffset, blockOffset + blockLength)} is fully covered
+ * with checksum-aligned boundaries.
+ */
+ long computeAdjustedLength(long blockOffset, long blockLength, long
adjustedOffset) {
+ // We use an inclusive blockEnd to straightforwardly identify which chunk
and checksum
+ // cover the final byte.
+ // For example, if bytesPerChecksum = 16 and we read up to byte 36
(exclusive readEnd = 36),
+ // it correctly locates the chunk. However, calculating the checksum index
from an
+ // exclusive boundary requires extra +1/-1 edge-case handling,
specifically when the
+ // end aligns exactly on a checksum boundary.
+ // Using the inclusive index makes ((blockEnd - chunkOffset) /
bytesPerChecksum) straightforward.
+ long blockEnd = blockOffset + blockLength - 1;
+ ChunkInfo lastChunk = chunks.get(searchChunk(blockEnd, chunks));
+ long chunkOffset = lastChunk.getOffset();
+
+ long roundDown = (blockEnd - chunkOffset) & bitMask;
+ long roundUp = roundDown + bytesPerChecksum;
+ long chunkLength = Math.min(roundUp, lastChunk.getLen());
+ return chunkOffset + chunkLength - adjustedOffset;
+ }
+
+ /**
+ * Compute the buffer limit for the next read iteration, aligned to
+ * checksum boundaries so that partial checksums are not sent across
+ * response messages.
+ */
+ public int computeBufferLimit(long offset, long remainingLength) {
+ if (responseDataSize >= remainingLength) {
+ return Math.toIntExact(remainingLength);
+ }
+
+ /*
+ * Use an exclusive boundary here. If we used an inclusive boundary
+ * that perfectly aligned with the start of the next chunk, it would
+ * incorrectly truncate the buffer limit.
+ *
+ * Example Scenario:
+ * - bytesPerChecksum = 16 (bitMask = -16)
+ * - Chunk 1: offset = 0, length = 20
+ * - Chunk 2: offset = 20, length = 20
+ * - Current offset = 0, responseDataSize = 20
+ *
+ * If using an INCLUSIVE boundary (end = 20 - 1 = 19):
+ * - endChunk = findChunk(19) -> Chunk 1 (offset = 0)
+ * - lengthExcludingEndChunk = 0 - 0 = 0
+ * - Result = ((20 - 0) & -16) + 0 = 16 (Incorrectly truncates 4 bytes)
+ *
+ * If using an EXCLUSIVE boundary (end = 20):
+ * - endChunk = findChunk(20) -> Chunk 2 (offset = 20)
+ * - lengthExcludingEndChunk = 20 - 0 = 20
+ * - Result = ((20 - 20) & -16) + 20 = 20 (Correctly reads the entire
Chunk 1)
+ */
+ ChunkInfo endChunk = chunks.get(findChunk(offset + responseDataSize));
+ final int lengthExcludingEndChunk = Math.toIntExact(endChunk.getOffset() -
offset);
+ // bytesPerChecksum must be a power of 2.
+ return ((responseDataSize - lengthExcludingEndChunk) & bitMask) +
lengthExcludingEndChunk;
+ }
+
+ /**
+ * @param position must be increasing in subsequent calls to this method.
+ * @return the chunk containing the given position.
+ */
+ int findChunk(long position) {
+ Preconditions.assertTrue(position >= lastPosition);
+ lastPosition = position;
+
+ for (; chunkIndex < chunks.size(); chunkIndex++) {
+ final ChunkInfo chunk = chunks.get(chunkIndex);
+ if (position >= chunk.getOffset() && position < chunk.getOffset() +
chunk.getLen()) {
+ return chunkIndex;
+ }
+ }
+ return chunkIndex;
+ }
+}
diff --git
a/hadoop-hdds/container-service/src/test/java/org/apache/hadoop/ozone/container/keyvalue/TestKeyValueHandler.java
b/hadoop-hdds/container-service/src/test/java/org/apache/hadoop/ozone/container/keyvalue/TestKeyValueHandler.java
index 4cc1bae8073..b4f9895a336 100644
---
a/hadoop-hdds/container-service/src/test/java/org/apache/hadoop/ozone/container/keyvalue/TestKeyValueHandler.java
+++
b/hadoop-hdds/container-service/src/test/java/org/apache/hadoop/ozone/container/keyvalue/TestKeyValueHandler.java
@@ -70,6 +70,7 @@
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Semaphore;
+import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.atomic.AtomicInteger;
import org.apache.commons.io.FileUtils;
import org.apache.hadoop.conf.StorageUnit;
@@ -90,6 +91,8 @@
import org.apache.hadoop.hdds.security.token.TokenVerifier;
import org.apache.hadoop.hdds.utils.io.RandomAccessFileChannel;
import org.apache.hadoop.metrics2.MetricsRecordBuilder;
+import org.apache.hadoop.ozone.common.Checksum;
+import org.apache.hadoop.ozone.common.ChecksumData;
import org.apache.hadoop.ozone.common.ChunkBuffer;
import org.apache.hadoop.ozone.container.ContainerTestHelper;
import org.apache.hadoop.ozone.container.checksum.ContainerChecksumTreeManager;
@@ -1120,7 +1123,7 @@ public void onCompleted() {
blockFile = new RandomAccessFileChannel();
ContainerCommandResponseProto response = kvHandler.readBlock(
- readBlockRequest, container, blockFile, streamObserver);
+ readBlockRequest, container, blockFile, streamObserver, false);
assertNull(response, "ReadBlock should return null on success");
assertTrue(responseCount.get() > 0, "Should receive at least one
response");
@@ -1198,7 +1201,7 @@ private void assertOutOfRange(ReadBlockResult result,
long offset) {
}
/**
- * Reads a block of {@code blockSize} bytes from a real container through
{@link KeyValueHandler#readBlock},
+ * Reads a block of {@code blockSize} bytes from a real container through
{@link Handler#readBlock},
* collecting the streamed responses and errors.
*/
private ReadBlockResult readBlock(int blockSize, long offset, long length)
throws Exception {
@@ -1244,7 +1247,7 @@ private ReadBlockResult readBlock(int blockSize, long
offset, long length) throw
ReadBlockResult result = new ReadBlockResult(blockID);
blockFile = new RandomAccessFileChannel();
- result.setResponse(kvHandler.readBlock(readBlockRequest, container,
blockFile, result));
+ result.setResponse(kvHandler.readBlock(readBlockRequest, container,
blockFile, result, false));
return result;
} finally {
if (blockFile != null) {
@@ -1338,4 +1341,194 @@ boolean deleteUnreferencedFile(File file) {
return false;
}
}
+
+ @Test
+ public void testReadBlockWithSmallChunks() throws Exception {
+ int[] chunkLens = {1, 1, 1, 1, 1};
+ Path testDir = Files.createTempDirectory("testReadBlock");
+ try {
+ HandlerWithVolumeSet handlerWithVolume =
createKeyValueHandlerWithVolumeSet(testDir);
+ long containerID = ContainerTestHelper.getTestContainerID();
+ KeyValueContainerData containerData = new KeyValueContainerData(
+ containerID, ContainerLayoutVersion.FILE_PER_BLOCK,
+ (long) StorageUnit.GB.toBytes(1), UUID.randomUUID().toString(),
+ DATANODE_UUID);
+ KeyValueContainer container = new KeyValueContainer(containerData, conf);
+ BlockID blockID =
ContainerTestHelper.getTestBlockID(container.getContainerData().getContainerID());
+ int totalLen = 0;
+ for (int len : chunkLens) {
+ totalLen += len;
+ }
+
+ byte[] rawData = new byte[totalLen];
+ ThreadLocalRandom.current().nextBytes(rawData);
+ writeBlock(handlerWithVolume, container, blockID, chunkLens, rawData);
+ readBlockAndVerify(handlerWithVolume, container, blockID, rawData, 2, 2,
2, 2);
+ } finally {
+ FileUtils.deleteDirectory(testDir.toFile());
+ ContainerMetrics.remove();
+ }
+ }
+
+ @Test
+ public void testReadBlockWithChunksNotMultipleOfBytesPerChecksum() throws
Exception {
+ int[] chunkLens = {1024, 20, 4096};
+ Path testDir = Files.createTempDirectory("testReadBlock");
+ try {
+ HandlerWithVolumeSet handlerWithVolume =
createKeyValueHandlerWithVolumeSet(testDir);
+ long containerID = ContainerTestHelper.getTestContainerID();
+ KeyValueContainerData containerData = new KeyValueContainerData(
+ containerID, ContainerLayoutVersion.FILE_PER_BLOCK,
+ (long) StorageUnit.GB.toBytes(1), UUID.randomUUID().toString(),
+ DATANODE_UUID);
+ KeyValueContainer container = new KeyValueContainer(containerData, conf);
+ BlockID blockID =
ContainerTestHelper.getTestBlockID(container.getContainerData().getContainerID());
+ int totalLen = 0;
+ for (int len : chunkLens) {
+ totalLen += len;
+ }
+
+ byte[] rawData = new byte[totalLen];
+ ThreadLocalRandom.current().nextBytes(rawData);
+ writeBlock(handlerWithVolume, container, blockID, chunkLens, rawData);
+ readBlockAndVerify(handlerWithVolume, container, blockID, rawData, 2048,
2048, 1044, 3072);
+ } finally {
+ FileUtils.deleteDirectory(testDir.toFile());
+ ContainerMetrics.remove();
+ }
+ }
+
+ @Test
+ public void testReadBlockMultipleResponse() throws Exception {
+ int[] chunkLens = {(1 << 20) + 10, 20, 4096};
+ Path testDir = Files.createTempDirectory("testReadBlock");
+ try {
+ HandlerWithVolumeSet handlerWithVolume =
createKeyValueHandlerWithVolumeSet(testDir);
+ long containerID = ContainerTestHelper.getTestContainerID();
+ KeyValueContainerData containerData = new KeyValueContainerData(
+ containerID, ContainerLayoutVersion.FILE_PER_BLOCK,
+ (long) StorageUnit.GB.toBytes(1), UUID.randomUUID().toString(),
+ DATANODE_UUID);
+ KeyValueContainer container = new KeyValueContainer(containerData, conf);
+ BlockID blockID =
ContainerTestHelper.getTestBlockID(container.getContainerData().getContainerID());
+ int totalLen = 0;
+ for (int len : chunkLens) {
+ totalLen += len;
+ }
+
+ byte[] rawData = new byte[totalLen];
+ ThreadLocalRandom.current().nextBytes(rawData);
+ writeBlock(handlerWithVolume, container, blockID, chunkLens, rawData);
+ readBlockAndVerify(handlerWithVolume, container, blockID, rawData, 20,
(1 << 20) + 20, 0, (1 << 20) + 30 + 1024);
+ } finally {
+ FileUtils.deleteDirectory(testDir.toFile());
+ ContainerMetrics.remove();
+ }
+
+ }
+
+ /**
+ * Creates a container with the given chunk layout, generates random data,
+ * computes real CRC32 checksums, writes each chunk to disk, and persists
+ * the block metadata.
+ */
+ private void writeBlock(HandlerWithVolumeSet handlerWithVolume,
KeyValueContainer container,
+ BlockID blockID, int[] chunkLens, byte[] rawData) throws Exception {
+ KeyValueHandler kvHandler = handlerWithVolume.getHandler();
+ MutableVolumeSet volumeSet = handlerWithVolume.getVolumeSet();
+ ContainerSet containerSet = handlerWithVolume.getContainerSet();
+
+ container.create(volumeSet, new RoundRobinVolumeChoosingPolicy(),
CLUSTER_ID);
+ containerSet.addContainer(container);
+
+ int bytesPerChecksum = 1024;
+
+ BlockData blockData = new BlockData(blockID);
+
+ long offset = 0;
+ Checksum checksum = new Checksum(ContainerProtos.ChecksumType.CRC32,
bytesPerChecksum);
+ for (int c = 0; c < chunkLens.length; c++) {
+ int len = chunkLens[c];
+ byte[] chunkBytes = new byte[len];
+ System.arraycopy(rawData, (int) offset, chunkBytes, 0, len);
+
+ ChunkBuffer chunkData = ChunkBuffer.wrap(ByteBuffer.wrap(chunkBytes));
+ ChecksumData checksumData = checksum.computeChecksum(chunkData);
+ chunkData.rewind();
+
+ ChunkInfo chunkInfo = new ChunkInfo("chunk" + (c + 1), offset, len);
+ chunkInfo.setChecksumData(checksumData);
+ blockData.addChunk(chunkInfo.getProtoBufMessage());
+
+ kvHandler.getChunkManager().writeChunk(container, blockID, chunkInfo,
chunkData,
+ DispatcherContext.getHandleWriteChunk());
+ offset += len;
+ }
+ kvHandler.getBlockManager().putBlock(container, blockData);
+ }
+
+ /**
+ * Reads the block at the given offset/length via
+ * {@link Handler#readBlock} and verifies that the response data
+ * matches the original bytes written by {@link #writeBlock}.
+ */
+ @SuppressWarnings("checkstyle:ParameterNumber")
+ private void readBlockAndVerify(HandlerWithVolumeSet handlerWithVolume,
KeyValueContainer container,
+ BlockID blockID, byte[] rawData, long readOffset, long length, long
readBackOffset, long readBackLength) {
+ ContainerCommandRequestProto readBlockRequest =
+ ContainerCommandRequestProto.newBuilder()
+ .setCmdType(ContainerProtos.Type.ReadBlock)
+ .setContainerID(container.getContainerData().getContainerID())
+ .setDatanodeUuid(DATANODE_UUID)
+ .setReadBlock(ContainerProtos.ReadBlockRequestProto.newBuilder()
+ .setBlockID(blockID.getDatanodeBlockIDProtobuf())
+ .setOffset(readOffset)
+ .setLength(length)
+ .build())
+ .build();
+
+ @SuppressWarnings("unchecked")
+ StreamObserver<ContainerCommandResponseProto> streamObserver =
mock(StreamObserver.class);
+ List<ContainerCommandResponseProto> capturedResponses = new
java.util.ArrayList<>();
+ doAnswer(invocation -> {
+ capturedResponses.add(invocation.getArgument(0));
+ return null;
+ }).when(streamObserver).onNext(any(ContainerCommandResponseProto.class));
+
+ try (RandomAccessFileChannel blockFile = new RandomAccessFileChannel()) {
+ ContainerCommandResponseProto response =
handlerWithVolume.getHandler().readBlock(
+ readBlockRequest, container, blockFile, streamObserver, true);
+
+ assertNull(response, "ReadBlock should return null on success");
+ }
+
+ assertFalse(capturedResponses.isEmpty(), "Should receive at least one
response");
+ verify(streamObserver, atLeastOnce()).onNext(any());
+
+ int returnedDataLen = 0;
+ for (ContainerCommandResponseProto resp : capturedResponses) {
+ returnedDataLen += resp.getReadBlock().getData().size();
+ }
+ ByteBuffer allData = ByteBuffer.allocate(returnedDataLen);
+ long firstResponseOffset =
capturedResponses.get(0).getReadBlock().getOffset();
+
+ for (ContainerCommandResponseProto resp : capturedResponses) {
+ assertEquals(ContainerProtos.Result.SUCCESS, resp.getResult());
+ assertTrue(resp.hasReadBlock());
+ allData.put(resp.getReadBlock().getData().asReadOnlyByteBuffer());
+ }
+
+ assertEquals(readBackOffset, firstResponseOffset);
+ assertEquals(readBackLength, returnedDataLen);
+
+ // Verify the returned data matches the expected slice of the original data
+ allData.flip();
+ byte[] readBack = new byte[allData.remaining()];
+ allData.get(readBack);
+ for (int i = 0; i < readBack.length; i++) {
+ assertEquals(rawData[(int) firstResponseOffset + i], readBack[i],
+ "Data mismatch at returned byte index " + i);
+ }
+ }
+
}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]