This is an automated email from the ASF dual-hosted git repository.
smengcl 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 d319875ec11 HDDS-16258. Refactor streaming block reads and fix
checksum verification for variable-sized chunks (#11302)
d319875ec11 is described below
commit d319875ec118ebcb608dbacfd7c1a7802948e4b7
Author: Peter Lee <[email protected]>
AuthorDate: Thu Oct 8 04:39:12 2026 +0800
HDDS-16258. Refactor streaming block reads and fix checksum verification
for variable-sized chunks (#11302)
---
.../hdds/scm/storage/StreamBlockInputStream.java | 16 +--
.../scm/storage/TestStreamBlockInputStream.java | 116 +++++++++++++--
.../ContainerCommandResponseBuilders.java | 12 +-
.../hdds/scm/storage/ContainerProtocolCalls.java | 3 +-
.../org/apache/hadoop/ozone/common/Checksum.java | 58 ++++----
.../apache/hadoop/ozone/common/TestChecksum.java | 63 ++++++++
.../container/common/impl/HddsDispatcher.java | 2 +-
.../ozone/container/common/interfaces/Handler.java | 2 +-
.../ozone/container/keyvalue/BlockReadCursor.java | 125 ++++++++++++++++
.../ozone/container/keyvalue/KeyValueHandler.java | 124 +++-------------
.../container/keyvalue/ReadBlockComputation.java | 159 ---------------------
.../container/keyvalue/TestBlockReadCursor.java | 118 +++++++++++++++
.../container/keyvalue/TestKeyValueHandler.java | 142 +++++++++++++++---
.../src/main/proto/DatanodeClientProtocol.proto | 5 +-
.../ozone/client/rpc/read/TestStreamRead.java | 63 ++++++++
15 files changed, 663 insertions(+), 345 deletions(-)
diff --git
a/hadoop-hdds/client/src/main/java/org/apache/hadoop/hdds/scm/storage/StreamBlockInputStream.java
b/hadoop-hdds/client/src/main/java/org/apache/hadoop/hdds/scm/storage/StreamBlockInputStream.java
index 96e5fbaa781..8d936215b66 100644
---
a/hadoop-hdds/client/src/main/java/org/apache/hadoop/hdds/scm/storage/StreamBlockInputStream.java
+++
b/hadoop-hdds/client/src/main/java/org/apache/hadoop/hdds/scm/storage/StreamBlockInputStream.java
@@ -54,7 +54,6 @@
import org.apache.hadoop.hdds.utils.ConnectionFailureUtils;
import org.apache.hadoop.io.retry.RetryPolicy;
import org.apache.hadoop.ozone.common.Checksum;
-import org.apache.hadoop.ozone.common.ChecksumData;
import org.apache.hadoop.security.token.Token;
import org.apache.ratis.protocol.exceptions.TimeoutIOException;
import org.apache.ratis.thirdparty.com.google.protobuf.ByteString;
@@ -232,7 +231,7 @@ private int preadOnce(XceiverClientGrpc client,
StreamingReader reader, Pipeline
throw new IOException("Uninitialized StreamingReadResponse: " + blockID);
}
client.streamRead(ContainerProtocolCalls.buildReadBlockCommandProto(
- blockID, blockOffset, length, responseDataSize, tokenRef.get(),
pipeline), response);
+ blockID, blockOffset, length, responseDataSize, verifyChecksum,
tokenRef.get(), pipeline), response);
int copied = 0;
while (copied < length) {
@@ -475,7 +474,7 @@ synchronized void readBlockImpl(long length) throws
IOException {
throw new IOException("Uninitialized StreamingReadResponse: " + blockID);
}
xceiverClient.streamRead(ContainerProtocolCalls.buildReadBlockCommandProto(
- blockID, requestedLength, length, responseDataSize, tokenRef.get(),
pipelineRef.get()), r);
+ blockID, requestedLength, length, responseDataSize, verifyChecksum,
tokenRef.get(), pipelineRef.get()), r);
}
/**
@@ -705,8 +704,7 @@ public void
onNext(ContainerProtos.ContainerCommandResponseProto containerComman
try {
ByteBuffer data = readBlock.getData().asReadOnlyByteBuffer();
if (verifyChecksum) {
- ChecksumData checksumData =
ChecksumData.getFromProtoBuf(readBlock.getChecksumData());
- Checksum.verifyChecksum(data, checksumData, 0);
+ Checksum.validateChecksums(data, readBlock.getOffset(), 0,
readBlock.getChunkInfoListList());
}
offerToQueue(readBlock);
} catch (Exception e) {
@@ -718,7 +716,7 @@ public void
onNext(ContainerProtos.ContainerCommandResponseProto containerComman
LOG.warn("Failed to process block {} response at offset={}, size={}:
{}, {}",
getBlockID().getContainerBlockID(),
offset, data.size(),
StringUtils.bytes2Hex(data.asReadOnlyByteBuffer(), 10),
- readBlock.getChecksumData(), e);
+ readBlock.getChunkInfoListList(), e);
if (r != null) {
r.getRequestObserver().onError(e);
}
@@ -792,10 +790,8 @@ private void setCompleted() {
private void offerToQueue(ReadBlockResponseProto item) {
if (LOG.isTraceEnabled()) {
- final ContainerProtos.ChecksumData checksumData =
item.getChecksumData();
- LOG.trace("{}: enqueue response offset {}, length {}, numChecksums {},
bytesPerChecksum={}",
- name, item.getOffset(), item.getData().size(),
- checksumData.getChecksumsList().size(),
checksumData.getBytesPerChecksum());
+ LOG.trace("{}: enqueue response offset {}, length {}, chunks {}",
+ name, item.getOffset(), item.getData().size(),
item.getChunkInfoListCount());
}
final boolean offered = responseQueue.offer(item);
Preconditions.assertTrue(offered, () -> "Failed to offer " + item);
diff --git
a/hadoop-hdds/client/src/test/java/org/apache/hadoop/hdds/scm/storage/TestStreamBlockInputStream.java
b/hadoop-hdds/client/src/test/java/org/apache/hadoop/hdds/scm/storage/TestStreamBlockInputStream.java
index 0effa12e4bf..6132f1e9dc5 100644
---
a/hadoop-hdds/client/src/test/java/org/apache/hadoop/hdds/scm/storage/TestStreamBlockInputStream.java
+++
b/hadoop-hdds/client/src/test/java/org/apache/hadoop/hdds/scm/storage/TestStreamBlockInputStream.java
@@ -68,6 +68,7 @@
import
org.apache.hadoop.hdds.scm.container.common.helpers.StorageContainerException;
import org.apache.hadoop.hdds.scm.pipeline.Pipeline;
import org.apache.hadoop.hdds.security.token.OzoneBlockTokenIdentifier;
+import org.apache.hadoop.ozone.common.Checksum;
import org.apache.hadoop.ozone.common.OzoneChecksumException;
import org.apache.hadoop.security.token.Token;
import org.apache.ratis.protocol.exceptions.TimeoutIOException;
@@ -76,6 +77,7 @@
import org.apache.ratis.thirdparty.io.grpc.StatusRuntimeException;
import org.apache.ratis.thirdparty.io.grpc.stub.ClientCallStreamObserver;
import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
/**
* Tests for StreamBlockInputStream custom configuration behavior.
@@ -1223,10 +1225,12 @@ private ReadBlockResponseProto
buildReadBlockResponse(byte[] data) {
return ReadBlockResponseProto.newBuilder()
.setOffset(0)
.setData(ByteString.copyFrom(data))
- .setChecksumData(ChecksumData.newBuilder()
- .setType(ContainerProtos.ChecksumType.NONE)
- .setBytesPerChecksum(data.length)
- .build())
+ .addChunkInfoList(ContainerProtos.ChunkInfo.newBuilder()
+ .setChunkName("chunk").setOffset(0).setLen(data.length)
+ .setChecksumData(ChecksumData.newBuilder()
+ .setType(ContainerProtos.ChecksumType.NONE)
+ .setBytesPerChecksum(data.length)
+ .build()))
.build();
}
@@ -1237,10 +1241,12 @@ private ContainerCommandResponseProto
buildResponseProto(byte[] data, long offse
.setReadBlock(ReadBlockResponseProto.newBuilder()
.setOffset(offset)
.setData(ByteString.copyFrom(data))
- .setChecksumData(ChecksumData.newBuilder()
- .setType(ContainerProtos.ChecksumType.NONE)
- .setBytesPerChecksum(data.length)
- .build())
+ .addChunkInfoList(ContainerProtos.ChunkInfo.newBuilder()
+ .setChunkName("chunk").setOffset(offset).setLen(data.length)
+ .setChecksumData(ChecksumData.newBuilder()
+ .setType(ContainerProtos.ChecksumType.NONE)
+ .setBytesPerChecksum(data.length)
+ .build()))
.build())
.build();
}
@@ -1252,12 +1258,96 @@ private ContainerCommandResponseProto
buildCorruptResponseProto(byte[] data, lon
.setReadBlock(ReadBlockResponseProto.newBuilder()
.setOffset(offset)
.setData(ByteString.copyFrom(data))
- .setChecksumData(ChecksumData.newBuilder()
- .setType(ContainerProtos.ChecksumType.CRC32)
- .setBytesPerChecksum(data.length)
- .addChecksums(ByteString.copyFrom(new byte[4]))
- .build())
+ .addChunkInfoList(ContainerProtos.ChunkInfo.newBuilder()
+ .setChunkName("chunk").setOffset(offset).setLen(data.length)
+ .setChecksumData(ChecksumData.newBuilder()
+ .setType(ContainerProtos.ChecksumType.CRC32)
+ .setBytesPerChecksum(data.length)
+ .addChecksums(ByteString.copyFrom(new byte[4]))
+ .build()))
.build())
.build();
}
+
+ private static ContainerProtos.ChunkInfo checksummedChunk(byte[] data, int
offset, int length) throws Exception {
+ return ContainerProtos.ChunkInfo.newBuilder().setChunkName("chunk-" +
offset).setOffset(offset).setLen(length)
+ .setChecksumData(new Checksum(ContainerProtos.ChecksumType.CRC32, 4)
+ .computeChecksum(ByteBuffer.wrap(data, offset,
length)).getProtoBufMessage()).build();
+ }
+
+ private static ContainerCommandResponseProto response(byte[] data, long
offset,
+ List<ContainerProtos.ChunkInfo> chunks) {
+ return
ContainerCommandResponseProto.newBuilder().setCmdType(Type.ReadBlock)
+
.setResult(ContainerProtos.Result.SUCCESS).setReadBlock(ReadBlockResponseProto.newBuilder()
+
.setOffset(offset).setData(ByteString.copyFrom(data)).addAllChunkInfoList(chunks)).build();
+ }
+
+ @Test
+ void testChunkChecksumsAndSeekPrefix() throws Exception {
+ byte[] data = new byte[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11};
+ List<ContainerProtos.ChunkInfo> chunks = Arrays.asList(
+ checksummedChunk(data, 0, 3), checksummedChunk(data, 3, 9));
+ for (int seek : new int[] {0, 4, 8, 11}) {
+ int offset = seek == 0 ? 0 : 3 + (seek - 3) / 4 * 4;
+ ContainerCommandResponseProto response =
response(Arrays.copyOfRange(data, offset, data.length), offset,
+ offset == 0 ? chunks : chunks.subList(1, 2));
+ assertStreamResponse(response, data, seek, true, false);
+ }
+ }
+
+ @Test
+ void testMissingMetadataAndVerificationDisabled() throws Exception {
+ byte[] data = new byte[] {0, 1, 2, 3};
+ ContainerCommandResponseProto missing = response(data, 0,
Collections.emptyList());
+ assertStreamResponse(missing, data, 0, true, true);
+ assertStreamResponse(missing, data, 0, false, false);
+ assertStreamResponse(buildResponseProto(data, 0), data, 0, true, false);
+ byte[] corruptData = data.clone();
+ corruptData[0]++;
+ ContainerCommandResponseProto corrupt = response(corruptData, 0,
+ Collections.singletonList(checksummedChunk(data, 0, data.length)));
+ assertStreamResponse(corrupt, data, 0, true, true);
+ assertStreamResponse(corrupt, corruptData, 0, false, false);
+ }
+
+ private void assertStreamResponse(ContainerCommandResponseProto response,
byte[] expected, int seek,
+ boolean verifyChecksum, boolean fails) throws Exception {
+ assertStreamResponse(response, expected, seek, verifyChecksum, fails,
false);
+ assertStreamResponse(response, expected, seek, verifyChecksum, fails,
true);
+ }
+
+ private void assertStreamResponse(ContainerCommandResponseProto response,
byte[] expected, int seek,
+ boolean verifyChecksum, boolean fails, boolean positioned) throws
Exception {
+ OzoneClientConfig config = newStreamReadConfig();
+ config.setChecksumVerify(verifyChecksum);
+ config.setMaxReadRetryCount(0);
+ ClientCallStreamObserver<ContainerCommandRequestProto> observer =
mock(ClientCallStreamObserver.class);
+ XceiverClientGrpc client = mockCapturingStreamingReadClient(observer,
reader -> reader.onNext(response));
+ XceiverClientFactory factory = mock(XceiverClientFactory.class);
+
when(factory.acquireClientForReadData(any(Pipeline.class))).thenReturn(client);
+ try (StreamBlockInputStream stream = new StreamBlockInputStream(new
BlockID(1, 16258), expected.length,
+ mockStandalonePipeline(), null, factory, NO_REFRESH, config)) {
+ stream.seek(seek);
+ byte[] actual = new byte[expected.length - seek];
+ if (fails) {
+ IOException error = assertThrows(IOException.class, () -> {
+ if (positioned) {
+ stream.readPositioned(seek, ByteBuffer.wrap(actual));
+ } else {
+ stream.read(actual);
+ }
+ });
+ assertThat(hasCause(error, OzoneChecksumException.class)).isTrue();
+ assertArrayEquals(new byte[actual.length], actual);
+ } else {
+ assertEquals(actual.length, positioned
+ ? stream.readPositioned(seek, ByteBuffer.wrap(actual)) :
stream.read(actual));
+ assertArrayEquals(Arrays.copyOfRange(expected, seek, expected.length),
actual);
+ }
+ }
+ ArgumentCaptor<ContainerCommandRequestProto> request =
ArgumentCaptor.forClass(ContainerCommandRequestProto.class);
+ verify(client).streamRead(request.capture(), any());
+ assertEquals(verifyChecksum,
request.getValue().getReadBlock().getIncludeChecksums());
+ verify(client, times(1)).completeStreamRead();
+ }
}
diff --git
a/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/scm/protocolPB/ContainerCommandResponseBuilders.java
b/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/scm/protocolPB/ContainerCommandResponseBuilders.java
index 831c899bde5..1ff6009d0c4 100644
---
a/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/scm/protocolPB/ContainerCommandResponseBuilders.java
+++
b/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/scm/protocolPB/ContainerCommandResponseBuilders.java
@@ -44,7 +44,6 @@
import org.apache.hadoop.hdds.protocol.datanode.proto.ContainerProtos.Result;
import org.apache.hadoop.hdds.protocol.datanode.proto.ContainerProtos.Type;
import
org.apache.hadoop.hdds.protocol.datanode.proto.ContainerProtos.WriteChunkResponseProto;
-import org.apache.hadoop.ozone.common.ChecksumData;
import org.apache.hadoop.ozone.common.ChunkBufferToByteString;
import org.apache.ratis.thirdparty.com.google.protobuf.ByteString;
import org.apache.ratis.thirdparty.com.google.protobuf.UnsafeByteOperations;
@@ -344,13 +343,14 @@ public static ContainerCommandResponseProto
getReadChunkResponse(
}
public static ContainerCommandResponseProto getReadBlockResponse(
- ContainerCommandRequestProto request, ChecksumData checksumData,
ByteBuffer data, long offset) {
+ ContainerCommandRequestProto request, List<ChunkInfo> chunks, ByteBuffer
data, long offset) {
- ContainerProtos.ReadBlockResponseProto response =
ContainerProtos.ReadBlockResponseProto.newBuilder()
- .setChecksumData(checksumData.getProtoBufMessage())
+ ContainerProtos.ReadBlockResponseProto.Builder response =
ContainerProtos.ReadBlockResponseProto.newBuilder()
.setData(ByteString.copyFrom(data))
- .setOffset(offset)
- .build();
+ .setOffset(offset);
+ if (request.getReadBlock().getIncludeChecksums()) {
+ response.addAllChunkInfoList(chunks);
+ }
return getSuccessResponseBuilder(request)
.setReadBlock(response)
diff --git
a/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/scm/storage/ContainerProtocolCalls.java
b/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/scm/storage/ContainerProtocolCalls.java
index b8d832cf45a..5e0f1b3bc9e 100644
---
a/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/scm/storage/ContainerProtocolCalls.java
+++
b/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/scm/storage/ContainerProtocolCalls.java
@@ -950,7 +950,7 @@ public static List<Validator> toValidatorList(Validator
validator) {
}
public static ContainerCommandRequestProto buildReadBlockCommandProto(
- BlockID blockID, long offset, long length, int responseDataSize,
+ BlockID blockID, long offset, long length, int responseDataSize, boolean
includeChecksums,
Token<? extends TokenIdentifier> token, Pipeline pipeline)
throws IOException {
final DatanodeDetails datanode = pipeline.getClosestNode();
@@ -959,6 +959,7 @@ public static ContainerCommandRequestProto
buildReadBlockCommandProto(
.setOffset(offset)
.setLength(length)
.setResponseDataSize(responseDataSize)
+ .setIncludeChecksums(includeChecksums)
.setBlockID(blockID.getDatanodeBlockIDProtobufBuilder(replicaIndex));
final ContainerCommandRequestProto.Builder builder =
ContainerCommandRequestProto.newBuilder().setCmdType(Type.ReadBlock)
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 128666784eb..a1b50a87d79 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,8 +17,6 @@
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;
@@ -441,37 +439,37 @@ public static void verifyChecksum(List<ByteBuffer>
bufferList, int startIndex, C
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) {
+ if (!data.hasRemaining()) {
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++;
+ int dataOffset = data.position();
+ int remaining = data.remaining();
+ long offset = blockOffset;
+ while (remaining > 0) {
+ if (startIndex >= chunks.size()) {
+ throw new OzoneChecksumException("Missing chunk metadata at offset " +
offset);
+ }
+ ChunkInfo chunk = chunks.get(startIndex++);
+ long chunkOffset = chunk.getOffset();
+ long length = chunk.getLen();
+ if (offset < chunkOffset || offset - chunkOffset >= length
+ || (offset != blockOffset && offset != chunkOffset)) {
+ throw new OzoneChecksumException("Invalid chunk coverage at offset " +
offset);
+ }
+ long relativeOffset = offset - chunkOffset;
+ int size = (int) Math.min(remaining, length - relativeOffset);
+ if (chunk.getChecksumData().getType() != ChecksumType.NONE) {
+ int bytesPerChecksum = chunk.getChecksumData().getBytesPerChecksum();
+ if (bytesPerChecksum <= 0 || relativeOffset % bytesPerChecksum != 0
+ || (relativeOffset + size != length && (relativeOffset + size) %
bytesPerChecksum != 0)) {
+ throw new OzoneChecksumException("Invalid checksum boundary at
offset " + offset);
+ }
+ verifySingleChunk(data, dataOffset, size, chunk, (int) (relativeOffset
/ bytesPerChecksum));
+ }
+ dataOffset += size;
+ offset += size;
+ remaining -= size;
}
-
}
private static void verifySingleChunk(ByteBuffer data, int dataOffset, int
dataLimit,
diff --git
a/hadoop-hdds/common/src/test/java/org/apache/hadoop/ozone/common/TestChecksum.java
b/hadoop-hdds/common/src/test/java/org/apache/hadoop/ozone/common/TestChecksum.java
index 7ccc5d8547e..57a35c21e57 100644
---
a/hadoop-hdds/common/src/test/java/org/apache/hadoop/ozone/common/TestChecksum.java
+++
b/hadoop-hdds/common/src/test/java/org/apache/hadoop/ozone/common/TestChecksum.java
@@ -25,7 +25,9 @@
import static org.mockito.Mockito.when;
import java.nio.ByteBuffer;
+import java.util.Arrays;
import java.util.Collections;
+import java.util.List;
import org.apache.commons.lang3.RandomStringUtils;
import org.apache.hadoop.hdds.protocol.datanode.proto.ContainerProtos;
import org.junit.jupiter.api.Test;
@@ -133,4 +135,65 @@ public void
testRejectsIncrementalBufferInWriteMode(boolean useChecksumCache) {
exception.getMessage());
}
}
+
+ private static ContainerProtos.ChunkInfo chunk(byte[] data, int offset, int
length, int interval) throws Exception {
+ return ContainerProtos.ChunkInfo.newBuilder().setChunkName("chunk-" +
offset).setOffset(offset).setLen(length)
+ .setChecksumData(new Checksum(ContainerProtos.ChecksumType.CRC32,
interval)
+ .computeChecksum(ByteBuffer.wrap(data, offset,
length)).getProtoBufMessage()).build();
+ }
+
+ @Test
+ void testChunkRelativeRangesPreserveBuffer() throws Exception {
+ byte[] data = "ABCDEFGHIJKL".getBytes(UTF_8);
+ List<ContainerProtos.ChunkInfo> chunks = Arrays.asList(chunk(data, 0, 3,
4), chunk(data, 3, 9, 4));
+ for (int start : new int[] {0, 3, 7, 11}) {
+ ByteBuffer buffer = ByteBuffer.wrap(data, start, data.length - start);
+ buffer.mark();
+ Checksum.validateChecksums(buffer, start, start == 0 ? 0 : 1, chunks);
+ assertEquals(start, buffer.position());
+ assertEquals(data.length, buffer.limit());
+ buffer.reset();
+ }
+ Checksum.validateChecksums(ByteBuffer.allocate(0), 0, 0,
Collections.emptyList());
+ data[8]++;
+ assertThrows(OzoneChecksumException.class,
+ () -> Checksum.validateChecksums(ByteBuffer.wrap(data), 0, 0, chunks));
+ }
+
+ @Test
+ void testMalformedChunkCoverageAndAlignment() throws Exception {
+ byte[] data = "ABCDEFGHIJKL".getBytes(UTF_8);
+ ContainerProtos.ChunkInfo first = chunk(data, 0, 3, 4);
+ ContainerProtos.ChunkInfo second = chunk(data, 3, 9, 4);
+ for (List<ContainerProtos.ChunkInfo> chunks : Arrays.asList(
+ Collections.<ContainerProtos.ChunkInfo>emptyList(),
Collections.singletonList(first),
+ Arrays.asList(second, first),
+ Arrays.asList(first, second.toBuilder().setOffset(2).build()),
+ Arrays.asList(first, second.toBuilder().setOffset(4).build()))) {
+ assertThrows(OzoneChecksumException.class,
+ () -> Checksum.validateChecksums(ByteBuffer.wrap(data), 0, 0,
chunks));
+ }
+ assertThrows(OzoneChecksumException.class, () ->
Checksum.validateChecksums(
+ ByteBuffer.wrap(data), -1, 0, Arrays.asList(first, second)));
+ for (int[] range : new int[][] {{4, 4}, {3, 3}}) {
+ assertThrows(OzoneChecksumException.class, () ->
Checksum.validateChecksums(
+ ByteBuffer.wrap(data, range[0], range[1]), range[0], 0,
Collections.singletonList(second)));
+ }
+ ContainerProtos.ChunkInfo invalid = second.toBuilder().setChecksumData(
+ second.getChecksumData().toBuilder().setBytesPerChecksum(0)).build();
+ assertThrows(OzoneChecksumException.class, () ->
Checksum.validateChecksums(
+ ByteBuffer.wrap(data, 3, 9), 3, 0,
Collections.singletonList(invalid)));
+ }
+
+ @Test
+ void testNoneAndDifferentChunkParameters() throws Exception {
+ byte[] data = "ABCDEFGHIJKL".getBytes(UTF_8);
+ ContainerProtos.ChunkInfo first = chunk(data, 0, 3, 2).toBuilder()
+ .setChecksumData(Checksum.getNoChecksumDataProto()).build();
+ List<ContainerProtos.ChunkInfo> chunks = Arrays.asList(first, chunk(data,
3, 9, 3));
+ Checksum.validateChecksums(ByteBuffer.wrap(data, 1, 11), 1, 0, chunks);
+ Checksum.validateChecksums(ByteBuffer.wrap(data), 0, 0,
+ Arrays.asList(chunk(data, 0, 3, 2), chunk(data, 3, 9, 3)));
+ }
+
}
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 34298c7263f..1564e3ddbf5 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, false);
+ responseProto = handler.readBlock(msg, container, blockFile,
streamObserver);
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 53dd03627e1..d6828423a36 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, boolean
testVariableChunks);
+ StreamObserver<ContainerCommandResponseProto> streamObserver);
}
diff --git
a/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/keyvalue/BlockReadCursor.java
b/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/keyvalue/BlockReadCursor.java
new file mode 100644
index 00000000000..e877039829a
--- /dev/null
+++
b/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/keyvalue/BlockReadCursor.java
@@ -0,0 +1,125 @@
+/*
+ * 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.io.IOException;
+import java.util.Arrays;
+import java.util.List;
+import
org.apache.hadoop.hdds.protocol.datanode.proto.ContainerProtos.ChecksumType;
+import
org.apache.hadoop.hdds.protocol.datanode.proto.ContainerProtos.ChunkInfo;
+
+/** Tracks chunk-relative checksum boundaries for a streaming block read. */
+class BlockReadCursor {
+ private static final int STREAMING_BYTES_PER_CHUNK = 1024 * 64;
+
+ private final List<ChunkInfo> chunks;
+ // [c1 offset, c2 offset, ..., cn offset, cn offset + cn len]
+ private final long[] chunkOffsets;
+ private final int responseDataSize;
+ private final long start;
+ private final long end;
+ private long offset;
+ private int chunkIndex;
+
+ BlockReadCursor(long requestedOffset, long length, int responseSize,
List<ChunkInfo> chunks) throws IOException {
+ this.chunks = chunks;
+ chunkOffsets = new long[chunks.size() + 1];
+ int bufferSize = responseSize;
+ for (int i = 0; i < chunks.size(); i++) {
+ ChunkInfo chunk = chunks.get(i);
+ chunkOffsets[i] = chunk.getOffset();
+ // One checksum interval (or the short chunk containing it) must always
fit in the buffer.
+ bufferSize = Math.max(bufferSize, (int) Math.min(chunk.getLen(),
interval(chunk)));
+ }
+ ChunkInfo lastChunk = chunks.get(chunks.size() - 1);
+ long blockEnd = lastChunk.getOffset() + lastChunk.getLen();
+ chunkOffsets[chunks.size()] = blockEnd;
+ this.responseDataSize = bufferSize;
+ chunkIndex = findChunk(requestedOffset);
+ start = length == 0 ? requestedOffset : alignDown(requestedOffset,
chunkIndex);
+ offset = start;
+ if (length == 0) {
+ end = start;
+ } else {
+ long requestedEnd = requestedOffset + Math.min(length, blockEnd -
requestedOffset);
+ int last = findChunk(requestedEnd - 1);
+ long floor = alignDown(requestedEnd - 1, last);
+ end = floor + Math.min(interval(chunks.get(last)), chunkOffsets[last +
1] - floor);
+ }
+ }
+
+ private static int interval(ChunkInfo chunk) throws IOException {
+ // Retain the existing read alignment for chunks without checksums.
+ if (chunk.getChecksumData().getType() == ChecksumType.NONE) {
+ return STREAMING_BYTES_PER_CHUNK;
+ }
+ int size = chunk.getChecksumData().getBytesPerChecksum();
+ if (size <= 0) {
+ throw new IOException("Invalid bytes per checksum: " + size);
+ }
+ return size;
+ }
+
+ private long alignDown(long position, int index) throws IOException {
+ return position - (position - chunkOffsets[index]) %
interval(chunks.get(index));
+ }
+
+ private int findChunk(long position) {
+ int index = Arrays.binarySearch(chunkOffsets, chunkIndex, chunks.size(),
position);
+ if (index >= 0) {
+ return index;
+ }
+ int insertionPoint = -index - 1;
+ return insertionPoint - 1;
+ }
+
+ long offset() {
+ return offset;
+ }
+
+ boolean hasRemaining() {
+ return offset < end;
+ }
+
+ int responseDataSize() {
+ return responseDataSize;
+ }
+
+ int nextReadLength() throws IOException {
+ long limit = offset + Math.min(responseDataSize, end - offset);
+ if (limit < end) {
+ limit = alignDown(limit, findChunk(limit));
+ }
+ return Math.toIntExact(limit - offset);
+ }
+
+ List<ChunkInfo> chunksForRead(int length) {
+ return chunks.subList(chunkIndex, findChunk(offset + length - 1) + 1);
+ }
+
+ void advance(int bytesRead) {
+ offset += bytesRead;
+ if (hasRemaining()) {
+ chunkIndex = findChunk(offset);
+ }
+ }
+
+ long bytesRead() {
+ return offset - start;
+ }
+}
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 bab8b03bbf8..b4342fcdc91 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
@@ -62,12 +62,12 @@
import static
org.apache.hadoop.ozone.container.common.impl.ContainerLayoutVersion.DEFAULT_LAYOUT;
import static
org.apache.hadoop.ozone.container.common.impl.ContainerLayoutVersion.FILE_PER_BLOCK;
import static
org.apache.hadoop.ozone.container.keyvalue.helpers.BlockUtils.getBlockMapKey;
-import static org.apache.ratis.util.Preconditions.assertSame;
import static org.apache.ratis.util.Preconditions.assertTrue;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.common.util.concurrent.Striped;
+import java.io.EOFException;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FilenameFilter;
@@ -104,7 +104,6 @@
import org.apache.hadoop.hdds.conf.StorageUnit;
import org.apache.hadoop.hdds.protocol.DatanodeDetails;
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.ContainerCommandRequestProto;
import
org.apache.hadoop.hdds.protocol.datanode.proto.ContainerProtos.ContainerCommandResponseProto;
import
org.apache.hadoop.hdds.protocol.datanode.proto.ContainerProtos.ContainerDataProto.State;
@@ -132,7 +131,6 @@
import org.apache.hadoop.ozone.OzoneConsts;
import org.apache.hadoop.ozone.client.io.BlockInputStreamFactoryImpl;
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.common.ChunkBufferToByteString;
import org.apache.hadoop.ozone.common.OzoneChecksumException;
@@ -188,7 +186,6 @@ public class KeyValueHandler extends Handler {
private static final Logger LOG = LoggerFactory.getLogger(
KeyValueHandler.class);
- private static final int STREAMING_BYTES_PER_CHUNK = 1024 * 64;
private final BlockManager blockManager;
private final ChunkManager chunkManager;
@@ -2284,7 +2281,7 @@ boolean deleteUnreferencedFile(File file) {
public ContainerCommandResponseProto readBlock(
ContainerCommandRequestProto request, Container kvContainer,
RandomAccessFileChannel blockFile,
- StreamObserver<ContainerCommandResponseProto> streamObserver, boolean
testVariableChunks) {
+ StreamObserver<ContainerCommandResponseProto> streamObserver) {
if (kvContainer.getContainerData().getLayoutVersion() != FILE_PER_BLOCK) {
return ContainerUtils.logAndReturnError(LOG,
@@ -2300,7 +2297,7 @@ public ContainerCommandResponseProto readBlock(
}
try {
final long startTime = Time.monotonicNow();
- final long bytesRead = readBlockImpl(request, blockFile, kvContainer,
streamObserver, testVariableChunks);
+ final long bytesRead = readBlockImpl(request, blockFile, kvContainer,
streamObserver);
KeyValueContainerData containerData = (KeyValueContainerData) kvContainer
.getContainerData();
HddsVolume volume = containerData.getVolume();
@@ -2324,7 +2321,7 @@ public ContainerCommandResponseProto readBlock(
}
private long readBlockImpl(ContainerCommandRequestProto request,
RandomAccessFileChannel blockFile,
- Container kvContainer, StreamObserver<ContainerCommandResponseProto>
streamObserver, boolean testVariableChunks)
+ Container kvContainer, StreamObserver<ContainerCommandResponseProto>
streamObserver)
throws IOException {
final ReadBlockRequestProto readBlock = request.getReadBlock();
int responseDataSize = readBlock.getResponseDataSize();
@@ -2348,76 +2345,26 @@ private long readBlockImpl(ContainerCommandRequestProto
request, RandomAccessFil
"Requested offset " + readBlock.getOffset() + " is beyond the end of
block " + blockID + " with size "
+ blockData.getSize()));
}
- final List<ContainerProtos.ChunkInfo> chunkInfos = blockData.getChunks();
- final ChecksumType checksumType =
chunkInfos.get(0).getChecksumData().getType();
- int bytesPerChecksum = STREAMING_BYTES_PER_CHUNK;
- if (checksumType != ContainerProtos.ChecksumType.NONE) {
- bytesPerChecksum =
chunkInfos.get(0).getChecksumData().getBytesPerChecksum();
- }
-
- // 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;
- Preconditions.checkState(adjustLength <= blockData.getSize() -
adjustedOffset);
- LOG.debug("adjustedOffset {}, requiredLength {}, blockSize {}",
- 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);
+ final BlockReadCursor cursor = new BlockReadCursor(readBlock.getOffset(),
readBlock.getLength(),
+ responseDataSize, blockData.getChunks());
+ final ByteBuffer buffer = ByteBuffer.allocate(cursor.responseDataSize());
+ blockFile.position(cursor.offset());
+ while (cursor.hasRemaining()) {
+ buffer.clear().limit(cursor.nextReadLength());
+ blockFile.read(buffer);
+ if (buffer.hasRemaining()) {
+ throw new EOFException("Unexpected end of block " + blockID + " at " +
cursor.offset());
+ }
buffer.flip();
final int readLength = buffer.remaining();
- if (readLength == 0) {
- assertTrue(!shouldRead);
- break;
- }
- assertTrue(readLength > 0, () -> "readLength = " + readLength + " <= 0");
-
- if (checksumType != ContainerProtos.ChecksumType.NONE) {
-
- 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 List<ContainerProtos.ChunkInfo> chunks =
cursor.chunksForRead(readLength);
+ if (validateChunkChecksumData) {
+ Checksum.validateChecksums(buffer, cursor.offset(), 0, chunks);
}
- final ContainerCommandResponseProto response = getReadBlockResponse(
- request, checksumData, buffer, adjustedOffset);
- final int dataLength = response.getReadBlock().getData().size();
- LOG.debug("server onNext response {}: dataLength={}, numChecksums={}",
- numResponses, dataLength,
response.getReadBlock().getChecksumData().getChecksumsList().size());
- streamObserver.onNext(response);
- buffer.clear();
-
- adjustedOffset += readLength;
- totalDataLength += dataLength;
- numResponses++;
- chunkIndex = readBlockComputation.findChunk(adjustedOffset);
+ streamObserver.onNext(getReadBlockResponse(request, chunks, buffer,
cursor.offset()));
+ cursor.advance(readLength);
}
- return totalDataLength;
+ return cursor.bytesRead();
}
/**
@@ -2432,37 +2379,6 @@ private static long
rejectReadBlock(RandomAccessFileChannel blockFile,
return 0;
}
- 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);
- for (int i = 0; i < numChecksums; i++) {
- // As the checksums are stored "chunk by chunk", we need to figure out
which chunk we start reading from,
- // and its offset to pull out the correct checksum bytes for each read.
- final int n = i * bytesPerChecksum;
- final long offset = blockOffset + n;
- final int c = Math.toIntExact(offset / bytesPerChunk);
- final int chunkOffset = Math.toIntExact(offset % bytesPerChunk);
- final int csi = chunkOffset / bytesPerChecksum;
-
- assertTrue(c < chunks.size(),
- () -> "chunkIndex = " + c + " >= chunk.size()" + chunks.size());
- final ContainerProtos.ChunkInfo chunk = chunks.get(c);
- if (c < chunks.size() - 1) {
- assertSame(bytesPerChunk, chunk.getLen(), "bytesPerChunk");
- }
- final ContainerProtos.ChecksumData checksumDataProto =
chunks.get(c).getChecksumData();
- assertSame(bytesPerChecksum, checksumDataProto.getBytesPerChecksum(),
"bytesPerChecksum");
- final List<ByteString> checksumsList =
checksumDataProto.getChecksumsList();
- assertTrue(csi < checksumsList.size(),
- () -> "checksumIndex = " + csi + " >= checksumsList.size()" +
checksumsList.size());
- checksums.add(checksumsList.get(csi));
- }
- return checksums;
- }
-
@Override
public void addFinalizedBlock(Container container, long localID) {
KeyValueContainer keyValueContainer = (KeyValueContainer)container;
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
deleted file mode 100644
index de5cb5e46fd..00000000000
---
a/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/keyvalue/ReadBlockComputation.java
+++ /dev/null
@@ -1,159 +0,0 @@
-/*
- * 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 = searchChunk(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 = searchChunk(20) -> Chunk 2 (offset = 20)
- * - lengthExcludingEndChunk = 20 - 0 = 20
- * - Result = ((20 - 20) & -16) + 20 = 20 (Correctly reads the entire
Chunk 1)
- *
- * Use searchChunk, not findChunk: the read may stop before this end, so
the findChunk cursor must not move here.
- */
- ChunkInfo endChunk = chunks.get(searchChunk(offset + responseDataSize,
chunks));
- 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/TestBlockReadCursor.java
b/hadoop-hdds/container-service/src/test/java/org/apache/hadoop/ozone/container/keyvalue/TestBlockReadCursor.java
new file mode 100644
index 00000000000..1dda101b8c7
--- /dev/null
+++
b/hadoop-hdds/container-service/src/test/java/org/apache/hadoop/ozone/container/keyvalue/TestBlockReadCursor.java
@@ -0,0 +1,118 @@
+/*
+ * 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 static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import org.apache.hadoop.hdds.protocol.datanode.proto.ContainerProtos;
+import
org.apache.hadoop.hdds.protocol.datanode.proto.ContainerProtos.ChunkInfo;
+import org.junit.jupiter.api.Test;
+
+/** Tests the response ranges selected by {@link BlockReadCursor}. */
+class TestBlockReadCursor {
+ private static ChunkInfo chunk(long offset, long length, int interval) {
+ return ChunkInfo.newBuilder().setChunkName("chunk-" +
offset).setOffset(offset).setLen(length)
+ .setChecksumData(ContainerProtos.ChecksumData.newBuilder()
+
.setType(ContainerProtos.ChecksumType.CRC32).setBytesPerChecksum(interval)).build();
+ }
+
+ @Test
+ void testShiftedRangesAndLookahead() throws Exception {
+ List<ChunkInfo> chunks = Arrays.asList(chunk(0, 3, 4), chunk(3, 9, 4),
chunk(12, 7, 3));
+ BlockReadCursor cursor = new BlockReadCursor(4, 14, 6, chunks);
+ assertEquals(3, cursor.offset());
+ assertEquals(4, cursor.nextReadLength());
+ assertEquals(4, cursor.nextReadLength());
+ assertEquals(chunks.subList(1, 2), cursor.chunksForRead(4));
+ cursor.advance(4);
+ assertEquals(7, cursor.offset());
+ assertEquals(5, cursor.nextReadLength());
+ cursor.advance(5);
+ assertEquals(12, cursor.offset());
+ assertEquals(6, cursor.nextReadLength());
+ cursor.advance(6);
+ assertFalse(cursor.hasRemaining());
+ assertEquals(15, cursor.bytesRead());
+ }
+
+ @Test
+ void testSpanningChunksAndShortFinalChecksum() throws Exception {
+ List<ChunkInfo> chunks = Arrays.asList(chunk(0, 3, 4), chunk(3, 9, 4));
+ BlockReadCursor cursor = new BlockReadCursor(0, Long.MAX_VALUE, 8, chunks);
+ assertEquals(7, cursor.nextReadLength());
+ assertEquals(chunks, cursor.chunksForRead(7));
+ cursor.advance(7);
+ assertEquals(5, cursor.nextReadLength());
+ cursor.advance(5);
+ assertFalse(cursor.hasRemaining());
+ assertEquals(12, cursor.bytesRead());
+ }
+
+ @Test
+ void testReadsAtChunkAndChecksumBoundaries() throws Exception {
+ List<ChunkInfo> chunks = Arrays.asList(chunk(0, 3, 4), chunk(3, 9, 4),
chunk(12, 7, 3));
+ for (int[] range : new int[][] {{0, 3, 0}, {3, 7, 1}, {7, 11, 1}, {11, 12,
1},
+ {12, 15, 2}, {15, 18, 2}, {18, 19, 2}}) {
+ for (int requestedOffset : new int[] {range[0], range[1] - 1}) {
+ BlockReadCursor cursor = new BlockReadCursor(requestedOffset, 1, 8,
chunks);
+ int length = range[1] - range[0];
+ assertEquals(range[0], cursor.offset());
+ assertEquals(length, cursor.nextReadLength());
+ assertEquals(Collections.singletonList(chunks.get(range[2])),
cursor.chunksForRead(length));
+ cursor.advance(length);
+ assertFalse(cursor.hasRemaining());
+ }
+ }
+ }
+
+ @Test
+ void testMinimumBufferAndExactChunkEnd() throws Exception {
+ List<ChunkInfo> chunks = Arrays.asList(chunk(0, 3, 4), chunk(3, 9, 4));
+ BlockReadCursor cursor = new BlockReadCursor(3, 9, 1, chunks);
+ for (int expected : new int[] {4, 4, 1}) {
+ assertTrue(cursor.hasRemaining());
+ assertEquals(expected, cursor.nextReadLength());
+ cursor.advance(expected);
+ }
+ assertFalse(cursor.hasRemaining());
+ assertFalse(new BlockReadCursor(3, 0, 1, chunks).hasRemaining());
+ BlockReadCursor shortChunk = new BlockReadCursor(0, 3, 1,
+ Collections.singletonList(chunk(0, 3, Integer.MAX_VALUE)));
+ assertEquals(3, shortChunk.responseDataSize());
+ assertEquals(3, shortChunk.nextReadLength());
+ }
+
+ @Test
+ void testRangeNearLongLimit() throws Exception {
+ List<ChunkInfo> chunks = Arrays.asList(chunk(0, Long.MAX_VALUE - 9, 4),
chunk(Long.MAX_VALUE - 9, 9, 4));
+ BlockReadCursor cursor = new BlockReadCursor(Long.MAX_VALUE - 8,
Long.MAX_VALUE, 4, chunks);
+ assertEquals(Long.MAX_VALUE - 9, cursor.offset());
+ for (int length : new int[] {4, 4, 1}) {
+ assertEquals(length, cursor.nextReadLength());
+ cursor.advance(length);
+ }
+ assertEquals(Long.MAX_VALUE, cursor.offset());
+ assertFalse(cursor.hasRemaining());
+ }
+
+}
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 c35c003141c..6a5bf67a66d 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
@@ -71,7 +71,6 @@
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;
@@ -133,6 +132,8 @@
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
import org.mockito.Mockito;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -1125,7 +1126,7 @@ public void onCompleted() {
blockFile = new RandomAccessFileChannel();
ContainerCommandResponseProto response = kvHandler.readBlock(
- readBlockRequest, container, blockFile, streamObserver, false);
+ readBlockRequest, container, blockFile, streamObserver);
assertNull(response, "ReadBlock should return null on success");
assertTrue(responseCount.get() > 0, "Should receive at least one
response");
@@ -1210,6 +1211,64 @@ public void
testReadBlockErrorOnTheStreamClosesTheBlockFile() throws Exception {
}
}
+ @Test
+ void testReadBlockResponseSize() throws Exception {
+ try (StreamFixture fixture = new StreamFixture()) {
+ fixture.appendChunk("chunk1", 0, BLOCK_SIZE);
+ for (int responseSize : new int[] {0, 64 * 1024 * 1024}) {
+ assertResponses(fixture.read(0, BLOCK_SIZE, responseSize), 0,
BLOCK_SIZE);
+ }
+ }
+ }
+
+ @ParameterizedTest
+ @ValueSource(booleans = {true, false})
+ void testReadBlockWithoutResponseChecksums(boolean validateChecksums) throws
Exception {
+ DatanodeConfiguration datanodeConfig =
conf.getObject(DatanodeConfiguration.class);
+ datanodeConfig.setChunkDataValidationCheck(validateChecksums);
+ conf.setFromObject(datanodeConfig);
+ try (StreamFixture fixture = new StreamFixture()) {
+ long offset = 0;
+ for (int length : new int[] {3, 9}) {
+ ChunkInfo chunk = new ChunkInfo("chunk-" + offset, offset, length);
+ chunk.setChecksumData(new Checksum(ContainerProtos.ChecksumType.CRC32,
4)
+ .computeChecksum(ByteBuffer.wrap(writtenBytes(offset, length))));
+ fixture.appendChunk(chunk);
+ offset += length;
+ }
+ ContainerProtos.ReadBlockRequestProto.Builder request =
ContainerProtos.ReadBlockRequestProto.newBuilder()
+ .setOffset(4).setLength(8).setResponseDataSize(4);
+ ReadBlockResult withChecksums = fixture.read(request);
+ assertResponses(withChecksums, 3, 9);
+ assertResponses(fixture.read(request.setIncludeChecksums(true)), 3, 9);
+ ReadBlockResult withoutChecksums =
fixture.read(request.setIncludeChecksums(false));
+ assertNull(withoutChecksums.getResponse());
+ assertThat(withoutChecksums.getErrors()).isEmpty();
+ assertEquals(withChecksums.getDataResponses().size(),
withoutChecksums.getDataResponses().size());
+ for (int i = 0; i < withChecksums.getDataResponses().size(); i++) {
+ ContainerProtos.ReadBlockResponseProto expected =
withChecksums.getDataResponses().get(i).getReadBlock();
+ assertEquals(expected.toBuilder().clearChunkInfoList().build(),
+ withoutChecksums.getDataResponses().get(i).getReadBlock());
+ }
+
+ File file = ContainerLayoutVersion.FILE_PER_BLOCK.getChunkFile(
+ fixture.container.getContainerData(), fixture.blockID, "unused");
+ try (java.io.RandomAccessFile corrupt = new
java.io.RandomAccessFile(file, "rw")) {
+ corrupt.seek(3);
+ corrupt.write(255);
+ }
+ ReadBlockResult corrupt = fixture.read(request);
+ if (validateChecksums) {
+ assertEquals(ContainerProtos.Result.IO_EXCEPTION,
corrupt.getResponse().getResult());
+ assertThat(corrupt.getDataResponses()).isEmpty();
+ } else {
+ assertNull(corrupt.getResponse());
+ assertThat(corrupt.getErrors()).isEmpty();
+ assertEquals((byte) 255,
corrupt.getDataResponses().get(0).getReadBlock().getData().byteAt(0));
+ }
+ }
+ }
+
/**
* A chunk boundary inside the response window that is not checksum aligned
makes a response shorter than
* responseDataSize; the next response must continue from where the short
one stopped.
@@ -1238,7 +1297,7 @@ private static byte[] writtenBytes(long offset, int
length) {
}
/** Asserts a successful read of {@code totalLength} contiguous bytes from
{@code firstOffset}. */
- private static void assertResponses(ReadBlockResult result, long
firstOffset, long totalLength) {
+ private static void assertResponses(ReadBlockResult result, long
firstOffset, long totalLength) throws Exception {
assertNull(result.getResponse());
assertThat(result.getErrors()).isEmpty();
long offset = firstOffset;
@@ -1247,6 +1306,8 @@ private static void assertResponses(ReadBlockResult
result, long firstOffset, lo
assertEquals(offset, response.getReadBlock().getOffset());
final ByteString data = response.getReadBlock().getData();
assertArrayEquals(writtenBytes(offset, data.size()), data.toByteArray());
+ Checksum.validateChecksums(data.asReadOnlyByteBuffer(), offset, 0,
+ response.getReadBlock().getChunkInfoListList());
offset += data.size();
}
assertEquals(totalLength, offset - firstOffset, "total bytes delivered");
@@ -1291,9 +1352,13 @@ private final class StreamFixture implements
AutoCloseable {
/** Write one more chunk and re-put the block with it. */
void appendChunk(String name, long offset, int length) throws Exception {
- ChunkInfo chunkInfo = new ChunkInfo(name, offset, length);
+ appendChunk(new ChunkInfo(name, offset, length));
+ }
+
+ void appendChunk(ChunkInfo chunkInfo) throws Exception {
blockData.addChunk(chunkInfo.getProtoBufMessage());
- ChunkBuffer data = ChunkBuffer.wrap(ByteBuffer.wrap(writtenBytes(offset,
length)));
+ ChunkBuffer data = ChunkBuffer.wrap(ByteBuffer.wrap(
+ writtenBytes(chunkInfo.getOffset(), (int) chunkInfo.getLen())));
kvHandler.getChunkManager().writeChunk(container, blockID, chunkInfo,
data,
DispatcherContext.getHandleWriteChunk());
kvHandler.getBlockManager().putBlock(container, blockData);
@@ -1305,19 +1370,19 @@ ReadBlockResult read(long offset, long length) {
}
ReadBlockResult read(long offset, long length, int responseDataSize) {
+ return read(ContainerProtos.ReadBlockRequestProto.newBuilder()
+
.setOffset(offset).setLength(length).setResponseDataSize(responseDataSize));
+ }
+
+ ReadBlockResult read(ContainerProtos.ReadBlockRequestProto.Builder
readBlock) {
ContainerCommandRequestProto request =
ContainerCommandRequestProto.newBuilder()
.setCmdType(ContainerProtos.Type.ReadBlock)
.setContainerID(container.getContainerData().getContainerID())
.setDatanodeUuid(DATANODE_UUID)
- .setReadBlock(ContainerProtos.ReadBlockRequestProto.newBuilder()
- .setBlockID(blockID.getDatanodeBlockIDProtobuf())
- .setOffset(offset)
- .setLength(length)
- .setResponseDataSize(responseDataSize)
- .build())
+
.setReadBlock(readBlock.setBlockID(blockID.getDatanodeBlockIDProtobuf()))
.build();
ReadBlockResult result = new ReadBlockResult(blockID);
- result.setResponse(kvHandler.readBlock(request, container, blockFile,
result, false));
+ result.setResponse(kvHandler.readBlock(request, container, blockFile,
result));
return result;
}
@@ -1443,7 +1508,7 @@ public void testReadBlockWithSmallChunks() throws
Exception {
}
byte[] rawData = new byte[totalLen];
- ThreadLocalRandom.current().nextBytes(rawData);
+ new java.util.Random(16258).nextBytes(rawData);
writeBlock(handlerWithVolume, container, blockID, chunkLens, rawData);
readBlockAndVerify(handlerWithVolume, container, blockID, rawData, 2, 2,
2, 2);
} finally {
@@ -1471,7 +1536,7 @@ public void
testReadBlockWithChunksNotMultipleOfBytesPerChecksum() throws Except
}
byte[] rawData = new byte[totalLen];
- ThreadLocalRandom.current().nextBytes(rawData);
+ new java.util.Random(16258).nextBytes(rawData);
writeBlock(handlerWithVolume, container, blockID, chunkLens, rawData);
readBlockAndVerify(handlerWithVolume, container, blockID, rawData, 2048,
2048, 1044, 3072);
} finally {
@@ -1480,8 +1545,12 @@ public void
testReadBlockWithChunksNotMultipleOfBytesPerChecksum() throws Except
}
}
- @Test
- public void testReadBlockMultipleResponse() throws Exception {
+ @ParameterizedTest
+ @ValueSource(booleans = {true, false})
+ public void testReadBlockMultipleResponse(boolean validateChecksums) throws
Exception {
+ DatanodeConfiguration datanodeConfig =
conf.getObject(DatanodeConfiguration.class);
+ datanodeConfig.setChunkDataValidationCheck(validateChecksums);
+ conf.setFromObject(datanodeConfig);
int[] chunkLens = {(1 << 20) + 10, 20, 4096};
Path testDir = Files.createTempDirectory("testReadBlock");
try {
@@ -1499,7 +1568,7 @@ public void testReadBlockMultipleResponse() throws
Exception {
}
byte[] rawData = new byte[totalLen];
- ThreadLocalRandom.current().nextBytes(rawData);
+ new java.util.Random(16258).nextBytes(rawData);
writeBlock(handlerWithVolume, container, blockID, chunkLens, rawData);
readBlockAndVerify(handlerWithVolume, container, blockID, rawData, 20,
(1 << 20) + 20, 0, (1 << 20) + 30 + 1024);
} finally {
@@ -1556,7 +1625,8 @@ private void writeBlock(HandlerWithVolumeSet
handlerWithVolume, KeyValueContaine
*/
@SuppressWarnings("checkstyle:ParameterNumber")
private void readBlockAndVerify(HandlerWithVolumeSet handlerWithVolume,
KeyValueContainer container,
- BlockID blockID, byte[] rawData, long readOffset, long length, long
readBackOffset, long readBackLength) {
+ BlockID blockID, byte[] rawData, long readOffset, long length, long
readBackOffset, long readBackLength)
+ throws Exception {
ContainerCommandRequestProto readBlockRequest =
ContainerCommandRequestProto.newBuilder()
.setCmdType(ContainerProtos.Type.ReadBlock)
@@ -1579,7 +1649,7 @@ private void readBlockAndVerify(HandlerWithVolumeSet
handlerWithVolume, KeyValue
try (RandomAccessFileChannel blockFile = new RandomAccessFileChannel()) {
ContainerCommandResponseProto response =
handlerWithVolume.getHandler().readBlock(
- readBlockRequest, container, blockFile, streamObserver, true);
+ readBlockRequest, container, blockFile, streamObserver);
assertNull(response, "ReadBlock should return null on success");
}
@@ -1591,12 +1661,22 @@ private void readBlockAndVerify(HandlerWithVolumeSet
handlerWithVolume, KeyValue
for (ContainerCommandResponseProto resp : capturedResponses) {
returnedDataLen += resp.getReadBlock().getData().size();
}
+ List<ContainerProtos.ChunkInfo> storedChunks =
handlerWithVolume.getHandler().getBlockManager()
+ .getBlock(container, blockID).getChunks();
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());
+ long responseOffset = resp.getReadBlock().getOffset();
+ long responseEnd = responseOffset + resp.getReadBlock().getData().size();
+ List<ContainerProtos.ChunkInfo> overlapping = storedChunks.stream()
+ .filter(chunk -> chunk.getOffset() < responseEnd &&
chunk.getOffset() + chunk.getLen() > responseOffset)
+ .collect(java.util.stream.Collectors.toList());
+ assertEquals(overlapping, resp.getReadBlock().getChunkInfoListList());
+
Checksum.validateChecksums(resp.getReadBlock().getData().asReadOnlyByteBuffer(),
+ resp.getReadBlock().getOffset(), 0,
resp.getReadBlock().getChunkInfoListList());
allData.put(resp.getReadBlock().getData().asReadOnlyByteBuffer());
}
@@ -1613,4 +1693,28 @@ private void readBlockAndVerify(HandlerWithVolumeSet
handlerWithVolume, KeyValue
}
}
+ @Test
+ void testReadBlockZeroLengthAndOversizedRange() throws Exception {
+ try (StreamFixture fixture = new StreamFixture()) {
+ fixture.appendChunk("chunk1", 0, BLOCK_SIZE);
+ assertResponses(fixture.read(1, 0), 1, 0);
+ assertResponses(fixture.read(1, Long.MAX_VALUE), 0, BLOCK_SIZE);
+ }
+ }
+
+ @Test
+ void testReadBlockUnexpectedEof() throws Exception {
+ try (StreamFixture fixture = new StreamFixture()) {
+ fixture.appendChunk("chunk1", 0, BLOCK_SIZE);
+ File file = ContainerLayoutVersion.FILE_PER_BLOCK.getChunkFile(
+ fixture.container.getContainerData(), fixture.blockID, "unused");
+ try (java.io.RandomAccessFile truncated = new
java.io.RandomAccessFile(file, "rw")) {
+ truncated.setLength(BLOCK_SIZE - 1);
+ }
+ ReadBlockResult result = fixture.read(0, BLOCK_SIZE);
+ assertEquals(ContainerProtos.Result.IO_EXCEPTION,
result.getResponse().getResult());
+ assertThat(result.getDataResponses()).isEmpty();
+ }
+ }
+
}
diff --git
a/hadoop-hdds/interface-client/src/main/proto/DatanodeClientProtocol.proto
b/hadoop-hdds/interface-client/src/main/proto/DatanodeClientProtocol.proto
index 72d50c975a5..98c47c07c3a 100644
--- a/hadoop-hdds/interface-client/src/main/proto/DatanodeClientProtocol.proto
+++ b/hadoop-hdds/interface-client/src/main/proto/DatanodeClientProtocol.proto
@@ -395,12 +395,15 @@ message ReadBlockRequestProto {
required uint64 offset = 2;
optional uint64 length = 3;
optional uint32 responseDataSize = 4;
+ optional bool includeChecksums = 5 [default = true];
}
message ReadBlockResponseProto {
- required ChecksumData checksumData = 1;
+ reserved 1;
+ reserved "checksumData";
required uint64 offset = 2;
required bytes data = 3;
+ repeated ChunkInfo chunkInfoList = 4;
}
message EchoRequestProto {
diff --git
a/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/client/rpc/read/TestStreamRead.java
b/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/client/rpc/read/TestStreamRead.java
index 4adb0567e13..3ce0af44527 100644
---
a/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/client/rpc/read/TestStreamRead.java
+++
b/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/client/rpc/read/TestStreamRead.java
@@ -19,6 +19,7 @@
import static
org.apache.hadoop.hdds.protocol.proto.HddsProtos.ReplicationFactor.ONE;
import static
org.apache.hadoop.ozone.OzoneConfigKeys.OZONE_CLIENT_BYTES_PER_CHECKSUM_MIN_SIZE;
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -38,6 +39,7 @@
import org.apache.hadoop.hdds.client.RatisReplicationConfig;
import org.apache.hadoop.hdds.conf.OzoneConfiguration;
import org.apache.hadoop.hdds.conf.StorageUnit;
+import
org.apache.hadoop.hdds.protocol.datanode.proto.ContainerProtos.ChunkInfo;
import org.apache.hadoop.hdds.scm.OzoneClientConfig;
import org.apache.hadoop.hdds.scm.ScmConfigKeys;
import org.apache.hadoop.hdds.scm.storage.StreamBlockInputStream;
@@ -51,8 +53,12 @@
import org.apache.hadoop.ozone.client.OzoneClientFactory;
import org.apache.hadoop.ozone.client.io.KeyInputStream;
import org.apache.hadoop.ozone.client.protocol.ClientProtocol;
+import org.apache.hadoop.ozone.container.common.helpers.BlockData;
import org.apache.hadoop.ozone.container.common.impl.ContainerData;
import org.apache.hadoop.ozone.container.common.impl.ContainerLayoutVersion;
+import org.apache.hadoop.ozone.container.common.interfaces.DBHandle;
+import org.apache.hadoop.ozone.container.keyvalue.KeyValueContainerData;
+import org.apache.hadoop.ozone.container.keyvalue.helpers.BlockUtils;
import org.apache.hadoop.ozone.om.BucketForTesting;
import org.apache.hadoop.ozone.om.helpers.OmKeyInfo;
import org.apache.hadoop.ozone.om.helpers.OmKeyLocationInfo;
@@ -145,6 +151,63 @@ void testReadKey256k() throws Exception {
runTestReadKey(KEY_SIZE, bytesPerChecksum);
}
+ @Test
+ void testUnevenFlushedChunks() throws Exception {
+ OzoneConfiguration conf = new OzoneConfiguration(cluster.getConf());
+ OzoneClientConfig config = conf.getObject(OzoneClientConfig.class);
+ config.setStreamReadBlock(true);
+ config.setChecksumVerify(true);
+ config.setBytesPerChecksum(16 * 1024);
+ config.setStreamReadResponseDataSize(32 * 1024);
+ config.setStreamBufferFlushDelay(false);
+ conf.setFromObject(config);
+ byte[] expected = new byte[8193 + 32771 + 65543];
+ new java.util.Random(16258).nextBytes(expected);
+ try (OzoneClient client = OzoneClientFactory.getRpcClient(conf)) {
+ BucketForTesting bucket = BucketForTesting.newBuilder(client).build();
+ String key = "uneven-flushed-chunks";
+ try (OutputStream out = bucket.delegate().createKey(key, expected.length,
+ RatisReplicationConfig.getInstance(ONE), Collections.emptyMap())) {
+ int offset = 0;
+ for (int length : new int[] {8193, 32771, 65543}) {
+ out.write(expected, offset, length);
+ out.flush();
+ offset += length;
+ }
+ }
+ OmKeyInfo info =
client.getProxy().getKeyInfo(bucket.delegate().getVolumeName(),
+ bucket.delegate().getName(), key, false);
+ List<OmKeyLocationInfo> locations =
info.getLatestVersionLocations().createLocationList();
+ assertEquals(1, locations.size());
+ BlockID blockID = locations.get(0).getBlockID();
+ KeyValueContainerData container = (KeyValueContainerData)
cluster.getHddsDatanodes().get(0)
+ .getDatanodeStateMachine().getContainer().getContainerSet()
+ .getContainer(blockID.getContainerID()).getContainerData();
+ List<ChunkInfo> chunks;
+ try (DBHandle db = BlockUtils.getDB(container, conf)) {
+ BlockData block = db.getStore().getBlockByID(blockID,
container.getBlockKey(blockID.getLocalID()));
+ chunks = block.getChunks();
+ }
+ assertTrue(chunks.size() >= 3, "flush must persist separate chunks");
+ assertTrue(chunks.stream().map(ChunkInfo::getLen).distinct().count() >
1, "stored chunks must be nonuniform");
+ long shiftedOffset = chunks.get(1).getOffset();
+ assertTrue(shiftedOffset % config.getBytesPerChecksum() != 0, "second
chunk must shift the checksum grid");
+ try (KeyInputStream in = bucket.getKeyInputStream(key)) {
+ assertTrue(in.isStreamBlockInputStream());
+ byte[] actual = new byte[expected.length];
+ org.apache.hadoop.io.IOUtils.readFully(in, actual, 0, actual.length);
+ assertArrayEquals(expected, actual);
+ for (long position : new long[] {shiftedOffset + 1, shiftedOffset +
16385, expected.length - 17, 0}) {
+ in.seek(position);
+ int length = Math.min(20000, expected.length - (int) position);
+ byte[] range = new byte[length];
+ org.apache.hadoop.io.IOUtils.readFully(in, range, 0, range.length);
+ assertArrayEquals(Arrays.copyOfRange(expected, (int) position, (int)
position + length), range);
+ }
+ }
+ }
+ }
+
void runTestReadKey(SizeInBytes keySize, SizeInBytes bytesPerChecksum)
throws Exception {
final List<HddsDatanodeService> datanodes = cluster.getHddsDatanodes();
assertEquals(1, datanodes.size());
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]