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 07e64df5c6f HDDS-15424. Support concurrent positional read (#11102)
07e64df5c6f is described below
commit 07e64df5c6f3c2196848c73f2598e2056ff5ebf5
Author: Tak Lon (Stephen) Wu <[email protected]>
AuthorDate: Tue Sep 15 11:01:43 2026 -0700
HDDS-15424. Support concurrent positional read (#11102)
---
.../hadoop/hdds/scm/storage/BlockInputStream.java | 120 +++++++++++++++-
.../hadoop/hdds/scm/storage/ChunkInputStream.java | 151 +++++++++++++++++----
.../hdds/scm/storage/LocalChunkInputStream.java | 48 +++++--
.../hdds/scm/storage/MultipartInputStream.java | 73 +++++++++-
.../hdds/scm/storage/DummyChunkInputStream.java | 22 +--
.../hdds/scm/storage/PositionedReadTestHelper.java | 92 +++++++++++++
.../hdds/scm/storage/TestBlockInputStream.java | 109 +++++++++++++++
.../hdds/scm/storage/TestChunkInputStream.java | 71 ++++++++++
.../hdds/scm/storage/TestMultipartInputStream.java | 126 +++++++++++++++++
9 files changed, 752 insertions(+), 60 deletions(-)
diff --git
a/hadoop-hdds/client/src/main/java/org/apache/hadoop/hdds/scm/storage/BlockInputStream.java
b/hadoop-hdds/client/src/main/java/org/apache/hadoop/hdds/scm/storage/BlockInputStream.java
index 6d5ac9c8cf2..57757cb79f7 100644
---
a/hadoop-hdds/client/src/main/java/org/apache/hadoop/hdds/scm/storage/BlockInputStream.java
+++
b/hadoop-hdds/client/src/main/java/org/apache/hadoop/hdds/scm/storage/BlockInputStream.java
@@ -23,6 +23,7 @@
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
+import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@@ -77,7 +78,7 @@ public class BlockInputStream extends
BlockExtendedInputStream {
private XceiverClientShortCircuit xceiverClientShortCircuit;
private final AtomicBoolean fallbackToGrpc = new AtomicBoolean(false);
private volatile FileInputStream blockFileInputStream;
- private boolean initialized = false;
+ private volatile boolean initialized = false;
// TODO: do we need to change retrypolicy based on exception.
private final RetryPolicy retryPolicy;
@@ -412,10 +413,10 @@ protected synchronized int
readWithStrategy(ByteReaderStrategy strategy)
// Get the current chunkStream and read data from it
ChunkInputStream current = chunkStreams.get(chunkIndex);
- int numBytesToRead = Math.min(len, (int)current.getRemaining());
+ int readLength = Math.min(len, (int)current.getRemaining());
int numBytesRead;
try {
- numBytesRead = strategy.readFromBlock(current, numBytesToRead);
+ numBytesRead = strategy.readFromBlock(current, readLength);
retries = 0;
// If we get a StorageContainerException or an IOException due to
// datanodes are not reachable, refresh to get the latest pipeline
@@ -443,14 +444,14 @@ protected synchronized int
readWithStrategy(ByteReaderStrategy strategy)
}
}
- if (numBytesRead != numBytesToRead) {
+ if (numBytesRead != readLength) {
// This implies that there is either data loss or corruption in the
// chunk entries. Even EOF in the current stream would be covered in
// this case.
throw new IOException(String.format(
- "Inconsistent read for chunkName=%s length=%d numBytesToRead= %d "
+
+ "Inconsistent read for chunkName=%s length=%d readLength= %d " +
"numBytesRead=%d", current.getChunkName(), current.getLength(),
- numBytesToRead, numBytesRead));
+ readLength, numBytesRead));
}
totalReadLen += numBytesRead;
len -= numBytesRead;
@@ -462,6 +463,111 @@ protected synchronized int
readWithStrategy(ByteReaderStrategy strategy)
return totalReadLen;
}
+ /**
+ * Stateless positioned read across this block's chunks. Fills up to
+ * {@code dst.remaining()} bytes starting from {@code blockRelativePosition}
+ * without mutating this stream's cursor ({@code chunkIndex},
+ * {@code blockPosition}) or the sequential chunk streams' buffered state.
+ * Each covering chunk is read through an ephemeral {@link ChunkInputStream}
+ * closed as soon as its bytes have been copied.
+ *
+ * @return bytes copied into {@code dst}, or {@link #EOF} at EOF
+ */
+ int readPositioned(long blockRelativePosition, ByteBuffer dst) throws
IOException {
+ if (blockRelativePosition < 0) {
+ return EOF;
+ }
+
+ if (!initialized) {
+ initialize();
+ }
+ final long[] offsets;
+ final BlockData currentBlockData;
+ final long blockLength;
+ synchronized (this) {
+ checkOpen();
+ if (chunkOffsets == null || blockData == null || blockRelativePosition
>= length) {
+ return EOF;
+ }
+
+ offsets = Arrays.copyOf(chunkOffsets, chunkOffsets.length);
+ currentBlockData = blockData;
+ blockLength = length;
+ }
+
+ final List<ChunkInfo> chunkInfos = currentBlockData.getChunksList();
+ int index = MultipartInputStream.binarySearchOffsetIndex(offsets,
blockRelativePosition);
+
+ long pos = blockRelativePosition;
+ int totalReadLen = 0;
+ while (dst.hasRemaining() && pos < blockLength && index <
chunkInfos.size()) {
+ final ChunkInfo chunkInfo = chunkInfos.get(index);
+ final long chunkOffset = pos - offsets[index];
+ final long readLength = Math.min(Math.min(dst.remaining(),
chunkInfo.getLen() - chunkOffset), blockLength - pos);
+ if (readLength > 0) {
+ final int numBytesRead = readChunkAt(chunkInfo, chunkOffset, (int)
readLength, dst);
+ totalReadLen += numBytesRead;
+ pos += numBytesRead;
+ }
+ index++;
+ }
+ return totalReadLen == 0 ? EOF : totalReadLen;
+ }
+
+ /**
+ * Read {@code readLength} bytes starting at {@code chunkOffset} of the
given chunk into {@code dst}
+ * through an ephemeral {@link ChunkInputStream}, retrying like {@link
#readWithStrategy(ByteReaderStrategy)}
+ * but with a retry counter local to this call.
+ */
+ private int readChunkAt(ChunkInfo chunkInfo, long chunkOffset, int
readLength, ByteBuffer dst) throws IOException {
+ final int startPosition = dst.position();
+ int preadRetries = 0;
+ while (true) {
+ final ChunkInputStream chunkStream;
+ synchronized (this) {
+ checkOpen();
+ chunkStream = createChunkInputStream(chunkInfo);
+ }
+ final int numBytesRead;
+ try {
+ final int oldLimit = dst.limit();
+ try {
+ dst.limit(startPosition + readLength);
+ numBytesRead = chunkStream.readPositioned(chunkOffset, dst);
+ } finally {
+ dst.limit(oldLimit);
+ }
+ } catch (SCMSecurityException ex) {
+ throw ex;
+ } catch (StorageContainerException ex) {
+ if (!shouldRetryRead(ex, retryPolicy, ++preadRetries)) {
+ throw ex;
+ }
+ handleReadError(ex);
+ dst.position(startPosition);
+ continue;
+ } catch (IOException ex) {
+ if (!shouldRetryRead(ex, retryPolicy, ++preadRetries)) {
+ throw ex;
+ }
+ if (isConnectivityIssue(ex)) {
+ handleReadError(ex);
+ }
+ dst.position(startPosition);
+ continue;
+ } finally {
+ chunkStream.close();
+ }
+
+ if (numBytesRead != readLength) {
+ throw new IOException(String.format(
+ "Inconsistent read for chunkName=%s length=%d readLength=%d
numBytesRead=%d",
+ chunkInfo.getChunkName(), chunkInfo.getLen(), readLength,
numBytesRead));
+ }
+ return numBytesRead;
+ }
+ }
+
/**
* Seeks the BlockInputStream to the specified position. If the stream is
* not initialized, save the seeked position via blockPosition. Otherwise,
@@ -630,7 +736,7 @@ private synchronized void storePosition() {
blockPosition = getPos();
}
- private void handleReadError(IOException cause) throws IOException {
+ private synchronized void handleReadError(IOException cause) throws
IOException {
releaseClient();
final List<ChunkInputStream> inputStreams = this.chunkStreams;
if (inputStreams != null) {
diff --git
a/hadoop-hdds/client/src/main/java/org/apache/hadoop/hdds/scm/storage/ChunkInputStream.java
b/hadoop-hdds/client/src/main/java/org/apache/hadoop/hdds/scm/storage/ChunkInputStream.java
index ee70a67dd85..f78adc85e41 100644
---
a/hadoop-hdds/client/src/main/java/org/apache/hadoop/hdds/scm/storage/ChunkInputStream.java
+++
b/hadoop-hdds/client/src/main/java/org/apache/hadoop/hdds/scm/storage/ChunkInputStream.java
@@ -26,6 +26,7 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
+import java.util.Objects;
import java.util.function.Supplier;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.hadoop.fs.ByteBufferReadable;
@@ -37,6 +38,7 @@
import
org.apache.hadoop.hdds.protocol.datanode.proto.ContainerProtos.ChunkInfo;
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.DatanodeBlockID;
import
org.apache.hadoop.hdds.protocol.datanode.proto.ContainerProtos.ReadChunkResponseProto;
import org.apache.hadoop.hdds.scm.XceiverClientFactory;
import org.apache.hadoop.hdds.scm.XceiverClientSpi;
@@ -47,6 +49,7 @@
import org.apache.hadoop.ozone.common.OzoneChecksumException;
import org.apache.hadoop.ozone.common.utils.BufferUtils;
import org.apache.hadoop.security.token.Token;
+import org.apache.hadoop.security.token.TokenIdentifier;
import org.apache.ratis.thirdparty.com.google.protobuf.ByteString;
/**
@@ -300,10 +303,17 @@ private void updateDatanodeBlockId(Pipeline pipeline)
throws IOException {
datanodeBlockID =
blockID.getDatanodeBlockIDProtobufBuilder(replicaIdx).build();
}
+ private DatanodeBlockID buildDatanodeBlockId(Pipeline pipeline) throws
IOException {
+ updateDatanodeBlockId(pipeline);
+ return datanodeBlockID;
+ }
+
/**
* Acquire new client if previous one was released.
+ *
+ * @return the held client after ensuring it is acquired
*/
- protected synchronized void acquireClient() throws IOException {
+ private synchronized void acquireClient() throws IOException {
if (xceiverClientFactory != null && xceiverClient == null) {
Pipeline pipeline = pipelineSupplier.get();
xceiverClient = xceiverClientFactory.acquireClientForReadData(pipeline);
@@ -374,29 +384,10 @@ private synchronized void readChunkFromContainer(int len)
throws IOException {
// successful read in adjustBufferPosition()
storePosition();
- long adjustedBuffersOffset, adjustedBuffersLen;
- if (verifyChecksum) {
- // Adjust the chunk offset and length to include required checksum
- // boundaries
- Pair<Long, Long> adjustedOffsetAndLength =
- computeChecksumBoundaries(startByteIndex, len);
- adjustedBuffersOffset = adjustedOffsetAndLength.getLeft();
- adjustedBuffersLen = adjustedOffsetAndLength.getRight();
- } else {
- // Read from the startByteIndex
- adjustedBuffersOffset = startByteIndex;
- adjustedBuffersLen = len;
- }
-
- // Adjust the chunkInfo so that only the required bytes are read from
- // the chunk.
- final ChunkInfo adjustedChunkInfo = ChunkInfo.newBuilder(chunkInfo)
- .setOffset(chunkInfo.getOffset() + adjustedBuffersOffset)
- .setLen(adjustedBuffersLen)
- .build();
+ final ChunkInfo adjustedChunkInfo = getChunkInfo(startByteIndex, len);
readChunkDataIntoBuffers(adjustedChunkInfo);
- bufferOffsetWrtChunkData = adjustedBuffersOffset;
+ bufferOffsetWrtChunkData = adjustedChunkInfo.getOffset() -
chunkInfo.getOffset();
// If the stream was seeked to position before, then the buffer
// position should be adjusted as the reads happen at checksum boundaries.
@@ -408,6 +399,24 @@ private synchronized void readChunkFromContainer(int len)
throws IOException {
adjustBufferPosition(startByteIndex - bufferOffsetWrtChunkData);
}
+ ChunkInfo getChunkInfo(long chunkRelativePosition, int toRead) {
+ final long adjustedOffset;
+ final long adjustedLen;
+ if (verifyChecksum) {
+ Pair<Long, Long> boundaries =
computeChecksumBoundaries(chunkRelativePosition, toRead);
+ adjustedOffset = boundaries.getLeft();
+ adjustedLen = boundaries.getRight();
+ } else {
+ adjustedOffset = chunkRelativePosition;
+ adjustedLen = toRead;
+ }
+
+ return ChunkInfo.newBuilder(chunkInfo)
+ .setOffset(chunkInfo.getOffset() + adjustedOffset)
+ .setLen(adjustedLen)
+ .build();
+ }
+
protected void readChunkDataIntoBuffers(ChunkInfo readChunkInfo)
throws IOException {
buffers = readChunk(readChunkInfo);
@@ -426,15 +435,101 @@ protected void readChunkDataIntoBuffers(ChunkInfo
readChunkInfo)
}
/**
- * Send RPC call to get the chunk from the container.
+ * Stateless positioned read of up to {@code dst.remaining()} bytes starting
+ * at {@code chunkRelativePosition} within this chunk. Unlike the buffered
+ * {@link #read} path, this does not read or mutate any of the instance's
+ * buffer/position state ({@code buffers}, {@code chunkPosition},
+ * {@code bufferOffsetWrtChunkData}, ...), so it is safe to call concurrently
+ * from multiple threads sharing the same stream.
+ *
+ * @param chunkRelativePosition start offset within this chunk
+ * @param dst destination buffer
+ * @return number of bytes copied into {@code dst}, or {@link #EOF} at EOF
+ */
+ int readPositioned(long chunkRelativePosition, ByteBuffer dst) throws
IOException {
+ if (chunkRelativePosition < 0 || chunkRelativePosition >= length) {
+ return EOF;
+ }
+ final int toRead = (int) Math.min(dst.remaining(), length -
chunkRelativePosition);
+ if (toRead == 0) {
+ return 0;
+ }
+
+ final ChunkInfo readChunkInfo = getChunkInfo(chunkRelativePosition,
toRead);
+ final long adjustedOffset = readChunkInfo.getOffset() -
chunkInfo.getOffset();
+
+ final long skip = chunkRelativePosition - adjustedOffset;
+ if (xceiverClientFactory == null) {
+ // for LocalChunkInputStream, which does not use a xceiver client, just
read the chunk directly
+ return copyRange(readChunk(readChunkInfo), skip, toRead, dst);
+ }
+
+ final Pipeline pipeline = pipelineSupplier.get();
+ final ContainerProtos.DatanodeBlockID bid = buildDatanodeBlockId(pipeline);
+ final XceiverClientSpi client =
xceiverClientFactory.acquireClientForReadData(pipeline);
+ try {
+ final ByteBuffer[] readBuffers = readChunk(readChunkInfo, client, bid,
validators, tokenSupplier.get());
+ return copyRange(readBuffers, skip, toRead, dst);
+ } finally {
+ xceiverClientFactory.releaseClientForReadData(client, false);
+ }
+ }
+
+ /**
+ * Copy {@code toCopy} bytes from {@code src} buffers, skipping the first
+ * {@code skip} bytes, into {@code dst}. Operates on duplicates so the source
+ * buffers' positions are left untouched.
+ */
+ private static int copyRange(ByteBuffer[] src, long skip, int toCopy,
ByteBuffer dst) {
+ long remainingSkip = skip;
+ int copied = 0;
+ for (ByteBuffer buffer : src) {
+ if (copied >= toCopy) {
+ break;
+ }
+ if (remainingSkip > 0) {
+ if (remainingSkip >= buffer.remaining()) {
+ remainingSkip -= buffer.remaining();
+ continue;
+ } else {
+ buffer.position(Math.toIntExact(buffer.position() + remainingSkip));
+ remainingSkip = 0;
+ }
+ }
+ int n = Math.min(buffer.remaining(), toCopy - copied);
+ if (n <= 0) {
+ continue;
+ }
+ buffer.limit(buffer.position() + n);
+ dst.put(buffer);
+ copied += n;
+ }
+ return copied;
+ }
+
+ /**
+ * Send RPC call to get the chunk from the container using the
sequential-read client held in
+ * {@link #xceiverClient}. Called by the buffered sequential read path via
+ * {@link #readChunkDataIntoBuffers}.
*/
@VisibleForTesting
- protected ByteBuffer[] readChunk(ChunkInfo readChunkInfo)
- throws IOException {
+ protected ByteBuffer[] readChunk(ChunkInfo readChunkInfo) throws IOException
{
+ return readChunk(readChunkInfo, xceiverClient, datanodeBlockID,
validators, tokenSupplier.get());
+ }
- ReadChunkResponseProto readChunkResponse =
- ContainerProtocolCalls.readChunk(xceiverClient, readChunkInfo,
datanodeBlockID, validators,
- tokenSupplier.get());
+ /**
+ * Send RPC call to get the chunk from the container using an explicitly
provided client.
+ * Used by the positioned-read path so callers hold a local reference to the
client
+ * rather than re-reading the shared {@link #xceiverClient} field after the
lock is released.
+ * Subclasses that do not use an xceiver client (e.g. local short-circuit
reads) should
+ * override this method to ignore {@code client} and read through their own
mechanism.
+ */
+ private static ByteBuffer[] readChunk(
+ ChunkInfo chunk, XceiverClientSpi client, DatanodeBlockID blockID,
+ List<Validator> validators, Token<? extends TokenIdentifier> token)
throws IOException {
+ Objects.requireNonNull(client, "client");
+ final ReadChunkResponseProto readChunkResponse =
ContainerProtocolCalls.readChunk(
+ client, chunk, blockID, validators, token);
if (readChunkResponse.hasData()) {
return readChunkResponse.getData().asReadOnlyByteBufferList()
diff --git
a/hadoop-hdds/client/src/main/java/org/apache/hadoop/hdds/scm/storage/LocalChunkInputStream.java
b/hadoop-hdds/client/src/main/java/org/apache/hadoop/hdds/scm/storage/LocalChunkInputStream.java
index 9de58ac7fe9..a6e429fd6e3 100644
---
a/hadoop-hdds/client/src/main/java/org/apache/hadoop/hdds/scm/storage/LocalChunkInputStream.java
+++
b/hadoop-hdds/client/src/main/java/org/apache/hadoop/hdds/scm/storage/LocalChunkInputStream.java
@@ -18,6 +18,7 @@
package org.apache.hadoop.hdds.scm.storage;
import com.google.common.annotations.VisibleForTesting;
+import java.io.EOFException;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
@@ -62,7 +63,9 @@ public class LocalChunkInputStream extends ChunkInputStream
LocalChunkInputStream(ChunkInfo chunkInfo, BlockID blockId,
XceiverClientFactory xceiverClientFactory,
Supplier<Pipeline> pipelineSupplier, boolean verifyChecksum,
Supplier<Token<?>> tokenSupplier,
XceiverClientShortCircuit xceiverClientShortCircuit, FileInputStream
blockInputStream) {
- super(chunkInfo, blockId, xceiverClientFactory, pipelineSupplier,
verifyChecksum, tokenSupplier);
+ // Pass null for xceiverClientFactory such readPositioned() is taken, both
routing reads
+ // and checksum verification are done locally.
+ super(chunkInfo, blockId, null, pipelineSupplier, verifyChecksum,
tokenSupplier);
this.chunkInfo = chunkInfo;
this.dataIn = blockInputStream.getChannel();
this.validator = this::validateChunk;
@@ -80,14 +83,43 @@ public class LocalChunkInputStream extends ChunkInputStream
protected ByteBuffer[] readChunk(ChunkInfo readChunkInfo)
throws IOException {
int bytesPerChecksum = chunkInfo.getChecksumData().getBytesPerChecksum();
- final ByteBuffer[] buffers =
BufferUtils.assignByteBuffers(readChunkInfo.getLen(),
- bytesPerChecksum);
- dataIn.position(readChunkInfo.getOffset()).read(buffers);
+ final ByteBuffer[] buffers =
BufferUtils.assignByteBuffers(readChunkInfo.getLen(), bytesPerChecksum);
+ readAtOffset(buffers, readChunkInfo.getOffset());
Arrays.stream(buffers).forEach(ByteBuffer::flip);
validator.accept(Arrays.asList(buffers), readChunkInfo);
return buffers;
}
+ /**
+ * Read into {@code buffers} starting at {@code fileOffset} using positional
+ * {@link FileChannel} reads so concurrent callers on different chunks (which
+ * share the same underlying channel) do not stomp each other's cursor.
+ */
+ private void readAtOffset(ByteBuffer[] buffers, long fileOffset) throws
IOException {
+ long pos = fileOffset;
+ for (ByteBuffer buffer : buffers) {
+ final int remaining = buffer.remaining();
+ final long read = readAtOffset(buffer, pos);
+ if (buffer.hasRemaining()) {
+ throw new EOFException("Read only " + read + "bytes but expected to
read " + remaining
+ + " bytes at offset " + pos + " for chunk " +
chunkInfo.getChunkName());
+ }
+ pos += read;
+ }
+ }
+
+ private long readAtOffset(ByteBuffer buffer, long fileOffset) throws
IOException {
+ long pos = fileOffset;
+ while (buffer.hasRemaining()) {
+ final int n = dataIn.read(buffer, pos);
+ if (n < 0) {
+ break;
+ }
+ pos += n;
+ }
+ return pos - fileOffset;
+ }
+
private void validateChunk(List<ByteBuffer> bufferList, ChunkInfo
readChunkInfo)
throws OzoneChecksumException {
if (verifyChecksum) {
@@ -105,12 +137,4 @@ private void validateChunk(List<ByteBuffer> bufferList,
ChunkInfo readChunkInfo)
Checksum.verifyChecksum(bufferList, startIndex, checksumData);
}
}
-
- /**
- * Acquire short-circuit local read client.
- */
- @Override
- protected synchronized void acquireClient() throws IOException {
- // do nothing, read data doesn't need short-circuit client
- }
}
diff --git
a/hadoop-hdds/client/src/main/java/org/apache/hadoop/hdds/scm/storage/MultipartInputStream.java
b/hadoop-hdds/client/src/main/java/org/apache/hadoop/hdds/scm/storage/MultipartInputStream.java
index e7c259ce464..584d97c778e 100644
---
a/hadoop-hdds/client/src/main/java/org/apache/hadoop/hdds/scm/storage/MultipartInputStream.java
+++
b/hadoop-hdds/client/src/main/java/org/apache/hadoop/hdds/scm/storage/MultipartInputStream.java
@@ -40,6 +40,7 @@ public class MultipartInputStream extends ExtendedInputStream
{
// List of PartInputStream, one for each part of the key
private final List<? extends PartInputStream> partStreams;
private final boolean isStreamBlockInputStream;
+ private final boolean statelessPositionedReadSupported;
// partOffsets[i] stores the index of the first data byte in
// partStream w.r.t the whole key data.
@@ -70,15 +71,19 @@ public MultipartInputStream(String keyName,
// Calculate and update the partOffsets
this.partOffsets = new long[inputStreams.size()];
+ boolean statelessSupported = !inputStreams.isEmpty();
int i = 0;
long streamLength = 0L;
for (PartInputStream partInputStream : inputStreams) {
this.partOffsets[i++] = streamLength;
if (isStreamBlockInputStream) {
Preconditions.assertInstanceOf(partInputStream,
StreamBlockInputStream.class);
+ } else if (statelessSupported && !(partInputStream instanceof
BlockInputStream)) {
+ statelessSupported = false;
}
streamLength += partInputStream.getLength();
}
+ this.statelessPositionedReadSupported = statelessSupported;
this.length = streamLength;
}
@@ -191,10 +196,19 @@ public synchronized void seek(long pos) throws
IOException {
@Override
public boolean readFully(long position, ByteBuffer buffer) throws
IOException {
- if (!isStreamBlockInputStream) {
- return false;
+ if (isStreamBlockInputStream) {
+ return readFullyStreamBlock(position, buffer);
}
+ return readFullyStateless(position, buffer);
+ }
+ /**
+ * Positioned read for the StreamBlock path. This emulates a positioned read
+ * with seek-read-restore on the shared stream cursor, so it is synchronized
+ * to keep concurrent callers from corrupting each other's position.
+ */
+ private synchronized boolean readFullyStreamBlock(long position,
+ ByteBuffer buffer) throws IOException {
final long oldPos = getPos();
seek(position);
try {
@@ -219,6 +233,61 @@ int readImpl(InputStream inputStream) throws IOException {
return true;
}
+ /**
+ * Stateless positioned read for the replicated (Ratis) path. Routes the read
+ * across the part {@link BlockInputStream}s using the immutable
+ * {@link #partOffsets} without seeking or mutating the shared cursor, so
+ * concurrent positioned reads run independently. Returns {@code false} (so
+ * the caller can fall back) when any part is not a {@link BlockInputStream},
+ * e.g. erasure coded parts.
+ */
+ private boolean readFullyStateless(long position, ByteBuffer buffer) throws
IOException {
+ if (!buffer.hasRemaining()) {
+ return true;
+ }
+ if (!statelessPositionedReadSupported) {
+ return false;
+ }
+
+ long pos = position;
+ int bytesRead = 0;
+ while (buffer.hasRemaining()) {
+ if (pos < 0 || pos >= length) {
+ if (bytesRead > 0) {
+ return true;
+ }
+ throw new EOFException("EOF encountered at pos: " + pos +
+ " for key: " + key);
+ }
+ int idx = binarySearchOffsetIndex(partOffsets, pos);
+ BlockInputStream part = (BlockInputStream) partStreams.get(idx);
+ long partPos = pos - partOffsets[idx];
+ int n = part.readPositioned(partPos, buffer);
+ if (n <= 0) {
+ if (bytesRead > 0) {
+ return true;
+ }
+ throw new EOFException("EOF encountered at pos: " + pos +
+ " for key: " + key);
+ }
+ bytesRead += n;
+ pos += n;
+ }
+ return true;
+ }
+
+ static int binarySearchOffsetIndex(long[] offsets, long pos) {
+ final int idx = Arrays.binarySearch(offsets, pos);
+ if (idx >= 0) {
+ // exact match: pos falls exactly on the start of chunk/part idx
+ return idx;
+ }
+ // binarySearch returns n = -insertionPoint - 1;
+ // insertionPoint is -n - 1
+ // the containing index is insertionPoint - 1.
+ return -idx - 2;
+ }
+
public synchronized void initialize() throws IOException {
// Pre-check that the stream has not been intialized already
if (initialized) {
diff --git
a/hadoop-hdds/client/src/test/java/org/apache/hadoop/hdds/scm/storage/DummyChunkInputStream.java
b/hadoop-hdds/client/src/test/java/org/apache/hadoop/hdds/scm/storage/DummyChunkInputStream.java
index 1ef6327c2a1..97ec08f4d12 100644
---
a/hadoop-hdds/client/src/test/java/org/apache/hadoop/hdds/scm/storage/DummyChunkInputStream.java
+++
b/hadoop-hdds/client/src/test/java/org/apache/hadoop/hdds/scm/storage/DummyChunkInputStream.java
@@ -19,7 +19,9 @@
import java.nio.ByteBuffer;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.List;
+import java.util.concurrent.atomic.AtomicReference;
import org.apache.hadoop.hdds.client.BlockID;
import
org.apache.hadoop.hdds.protocol.datanode.proto.ContainerProtos.ChunkInfo;
import org.apache.hadoop.hdds.scm.XceiverClientFactory;
@@ -34,8 +36,8 @@ public class DummyChunkInputStream extends ChunkInputStream {
private final byte[] chunkData;
- // Stores the read chunk data in each readChunk call
- private final List<ByteString> readByteBuffers = new ArrayList<>();
+ // Buffers from the most recent readChunk call.
+ private final AtomicReference<List<ByteString>> lastReadByteBuffers = new
AtomicReference<>();
public DummyChunkInputStream(ChunkInfo chunkInfo,
BlockID blockId,
@@ -54,7 +56,7 @@ protected ByteBuffer[] readChunk(ChunkInfo readChunkInfo) {
int bufferCapacity = readChunkInfo.getChecksumData().getBytesPerChecksum();
int bufferLen;
- readByteBuffers.clear();
+ List<ByteString> chunkBuffers = new ArrayList<>();
while (remainingToRead > 0) {
if (remainingToRead < bufferCapacity) {
bufferLen = remainingToRead;
@@ -64,19 +66,16 @@ protected ByteBuffer[] readChunk(ChunkInfo readChunkInfo) {
ByteString byteString = ByteString.copyFrom(chunkData,
offset, bufferLen);
- readByteBuffers.add(byteString);
+ chunkBuffers.add(byteString);
offset += bufferLen;
remainingToRead -= bufferLen;
}
- return BufferUtils.getReadOnlyByteBuffers(readByteBuffers)
- .toArray(new ByteBuffer[0]);
- }
+ lastReadByteBuffers.set(chunkBuffers);
- @Override
- protected void acquireClient() {
- // No action needed
+ return BufferUtils.getReadOnlyByteBuffers(chunkBuffers)
+ .toArray(new ByteBuffer[0]);
}
@Override
@@ -85,6 +84,7 @@ protected void releaseClient() {
}
public List<ByteString> getReadByteBuffers() {
- return readByteBuffers;
+ List<ByteString> last = lastReadByteBuffers.get();
+ return last != null ? last : Collections.emptyList();
}
}
diff --git
a/hadoop-hdds/client/src/test/java/org/apache/hadoop/hdds/scm/storage/PositionedReadTestHelper.java
b/hadoop-hdds/client/src/test/java/org/apache/hadoop/hdds/scm/storage/PositionedReadTestHelper.java
new file mode 100644
index 00000000000..fe5514698ff
--- /dev/null
+++
b/hadoop-hdds/client/src/test/java/org/apache/hadoop/hdds/scm/storage/PositionedReadTestHelper.java
@@ -0,0 +1,92 @@
+/*
+ * 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.hdds.scm.storage;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Shared helpers for concurrent positioned-read unit tests.
+ */
+public final class PositionedReadTestHelper {
+
+ public static final int THREAD_COUNT = 8;
+ public static final int ITERATIONS = 100;
+ public static final int BUFFER_SIZE = 4096;
+ public static final int SOURCE_SIZE = 512 * 1024;
+
+ /**
+ * Performs a positioned read at the given key-relative offset into {@code
buf}.
+ */
+ @FunctionalInterface
+ public interface PositionedReadAction {
+ void readAtOffset(int offset, ByteBuffer buf) throws Exception;
+ }
+
+ private PositionedReadTestHelper() {
+ }
+
+ public static void runConcurrentPositionedReads(byte[] source,
+ PositionedReadAction action) throws Exception {
+ ExecutorService pool = Executors.newFixedThreadPool(THREAD_COUNT);
+ try {
+ List<Future<?>> futures = new ArrayList<>();
+ for (int t = 0; t < THREAD_COUNT; t++) {
+ final int threadId = t;
+ futures.add(pool.submit((Callable<Void>) () -> {
+ for (int i = 0; i < ITERATIONS; i++) {
+ int offset = (threadId * 1000 + i * 17) % (source.length -
BUFFER_SIZE);
+ ByteBuffer buf = ByteBuffer.allocate(BUFFER_SIZE);
+ action.readAtOffset(offset, buf);
+ buf.flip();
+ byte[] expected = Arrays.copyOfRange(source, offset, offset +
BUFFER_SIZE);
+ byte[] actual = new byte[BUFFER_SIZE];
+ buf.get(actual);
+ assertArrayEquals(expected, actual,
+ "thread " + threadId + " offset " + offset);
+ }
+ return null;
+ }));
+ }
+ for (Future<?> future : futures) {
+ future.get(1, TimeUnit.MINUTES);
+ }
+ } finally {
+ pool.shutdownNow();
+ }
+ }
+
+ public static Throwable unwrapExecutionException(
+ ExecutionException executionException) {
+ Throwable cause = executionException.getCause();
+ while (cause instanceof RuntimeException && cause.getCause() != null) {
+ cause = cause.getCause();
+ }
+ return cause;
+ }
+}
diff --git
a/hadoop-hdds/client/src/test/java/org/apache/hadoop/hdds/scm/storage/TestBlockInputStream.java
b/hadoop-hdds/client/src/test/java/org/apache/hadoop/hdds/scm/storage/TestBlockInputStream.java
index 1b5e7667e84..baf4d7fef09 100644
---
a/hadoop-hdds/client/src/test/java/org/apache/hadoop/hdds/scm/storage/TestBlockInputStream.java
+++
b/hadoop-hdds/client/src/test/java/org/apache/hadoop/hdds/scm/storage/TestBlockInputStream.java
@@ -18,6 +18,8 @@
package org.apache.hadoop.hdds.scm.storage;
import static
org.apache.hadoop.hdds.protocol.datanode.proto.ContainerProtos.Result.CONTAINER_NOT_FOUND;
+import static
org.apache.hadoop.hdds.scm.storage.PositionedReadTestHelper.BUFFER_SIZE;
+import static
org.apache.hadoop.hdds.scm.storage.PositionedReadTestHelper.SOURCE_SIZE;
import static
org.apache.hadoop.hdds.scm.storage.TestChunkInputStream.generateRandomData;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -26,6 +28,7 @@
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.anyLong;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
@@ -312,6 +315,71 @@ void refreshesPipelineOnReadFailure(IOException ex) throws
Exception {
id -> newBlockLocationInfo);
}
+ @ParameterizedTest
+ @MethodSource("exceptionsTriggersRefresh")
+ void refreshesPipelineOnPositionedReadFailure(IOException ex) throws
Exception {
+ Pipeline pipeline = MockPipeline.createSingleNodePipeline();
+ BlockLocationInfo blockLocationInfo = mock(BlockLocationInfo.class);
+ when(blockLocationInfo.getPipeline()).thenReturn(pipeline);
+ Pipeline newPipeline = MockPipeline.createSingleNodePipeline();
+ BlockLocationInfo newBlockLocationInfo = mock(BlockLocationInfo.class);
+
+ testRefreshesPipelineOnPositionedReadFailure(ex, blockLocationInfo,
+ id -> newBlockLocationInfo);
+
+ when(newBlockLocationInfo.getPipeline()).thenReturn(newPipeline);
+ testRefreshesPipelineOnPositionedReadFailure(ex, blockLocationInfo,
+ id -> blockLocationInfo);
+
+ when(newBlockLocationInfo.getPipeline()).thenReturn(null);
+ testRefreshesPipelineOnPositionedReadFailure(ex, blockLocationInfo,
+ id -> newBlockLocationInfo);
+ }
+
+ private void testRefreshesPipelineOnPositionedReadFailure(IOException ex,
+ BlockLocationInfo blockLocationInfo,
+ Function<BlockID, BlockLocationInfo> refreshPipelineFunction)
+ throws Exception {
+ BlockID blockID = new BlockID(new ContainerBlockID(1, 1));
+ final int len = 200;
+ final ChunkInputStream stream =
throwingChunkInputStreamForPositionedRead(ex, len, true);
+
+ when(this.refreshFunction.apply(any()))
+ .thenAnswer(inv -> refreshPipelineFunction.apply(blockID));
+
+ try (BlockInputStream subject = createSubject(blockID,
+ blockLocationInfo.getPipeline(), stream)) {
+ subject.initialize();
+ ByteBuffer buf = ByteBuffer.allocate(len);
+ int bytesRead = subject.readPositioned(0, buf);
+ assertEquals(len, bytesRead);
+ verify(this.refreshFunction).apply(blockID);
+ } finally {
+ reset(this.refreshFunction);
+ }
+ }
+
+ private static ChunkInputStream throwingChunkInputStreamForPositionedRead(
+ IOException ex, int len, boolean succeedOnRetry) throws IOException {
+ final ChunkInputStream stream = mock(ChunkInputStream.class);
+ OngoingStubbing<Integer> stubbing =
+ when(stream.readPositioned(anyLong(), any(ByteBuffer.class)))
+ .thenThrow(ex);
+ if (succeedOnRetry) {
+ stubbing.thenAnswer(invocation -> {
+ ByteBuffer buffer = invocation.getArgument(1);
+ int n = Math.min(len, buffer.remaining());
+ for (int i = 0; i < n; i++) {
+ buffer.put((byte) 0);
+ }
+ return n;
+ });
+ }
+ when(stream.getRemaining()).thenReturn((long) len);
+ when(stream.getLength()).thenReturn((long) len);
+ return stream;
+ }
+
private void testRefreshesPipelineOnReadFailure(IOException ex,
BlockLocationInfo blockLocationInfo,
Function<BlockID, BlockLocationInfo> refreshPipelineFunction)
@@ -469,4 +537,45 @@ private static Stream<Arguments>
exceptionsTriggersRefresh() {
new StatusException(Status.UNAVAILABLE))))
);
}
+
+ @Test
+ public void testConcurrentPositionedRead() throws Exception {
+ try (BlockInputStream largeBlockStream = createLargeBlockStream()) {
+ largeBlockStream.initialize();
+ PositionedReadTestHelper.runConcurrentPositionedReads(blockData,
+ (offset, buf) -> {
+ int read = largeBlockStream.readPositioned(offset, buf);
+ if (read != BUFFER_SIZE) {
+ throw new AssertionError("expected " + BUFFER_SIZE + " bytes at
" + offset
+ + " but read " + read);
+ }
+ });
+ }
+ }
+
+ private BlockInputStream createLargeBlockStream() throws Exception {
+ refreshFunction = mock(Function.class);
+ BlockID blockID = new BlockID(new ContainerBlockID(1, 2));
+ checksum = new Checksum(ChecksumType.NONE, 1024);
+ chunks = new ArrayList<>(1);
+ chunkDataMap = new HashMap<>();
+ blockData = generateRandomData(SOURCE_SIZE);
+ blockSize = SOURCE_SIZE;
+ String chunkName = "large-chunk";
+ ChunkInfo chunkInfo = ChunkInfo.newBuilder()
+ .setChunkName(chunkName)
+ .setOffset(0)
+ .setLen(SOURCE_SIZE)
+ .setChecksumData(checksum.computeChecksum(
+ blockData, 0, SOURCE_SIZE).getProtoBufMessage())
+ .build();
+ chunkDataMap.put(chunkName, blockData);
+ chunks.add(chunkInfo);
+
+ OzoneClientConfig clientConfig = conf.getObject(OzoneClientConfig.class);
+ clientConfig.setChecksumVerify(false);
+ Pipeline pipeline = MockPipeline.createSingleNodePipeline();
+ return new DummyBlockInputStream(blockID, blockSize, pipeline, null,
+ null, refreshFunction, chunks, chunkDataMap, clientConfig);
+ }
}
diff --git
a/hadoop-hdds/client/src/test/java/org/apache/hadoop/hdds/scm/storage/TestChunkInputStream.java
b/hadoop-hdds/client/src/test/java/org/apache/hadoop/hdds/scm/storage/TestChunkInputStream.java
index 248ea865522..96d911319f0 100644
---
a/hadoop-hdds/client/src/test/java/org/apache/hadoop/hdds/scm/storage/TestChunkInputStream.java
+++
b/hadoop-hdds/client/src/test/java/org/apache/hadoop/hdds/scm/storage/TestChunkInputStream.java
@@ -18,6 +18,8 @@
package org.apache.hadoop.hdds.scm.storage;
import static
org.apache.hadoop.hdds.scm.protocolPB.ContainerCommandResponseBuilders.getReadChunkResponse;
+import static
org.apache.hadoop.hdds.scm.storage.PositionedReadTestHelper.BUFFER_SIZE;
+import static
org.apache.hadoop.hdds.scm.storage.PositionedReadTestHelper.SOURCE_SIZE;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -156,6 +158,75 @@ public void testPartialChunkRead() throws Exception {
matchWithInputData(chunkStream.getReadByteBuffers(), 0, 60);
}
+ @Test
+ public void testPositionedReadIsStateless() throws Exception {
+ // Advance the stream's own cursor with a normal sequential read.
+ byte[] seq = new byte[10];
+ assertEquals(10, chunkStream.read(seq, 0, 10));
+ matchWithInputData(seq, 0, 10);
+ long posBeforePositionedRead = chunkStream.getPos();
+
+ // A positioned read at an unrelated offset returns the right bytes ...
+ ByteBuffer dst = ByteBuffer.allocate(15);
+ int read = chunkStream.readPositioned(50, dst);
+ assertEquals(15, read);
+ dst.flip();
+ byte[] positioned = new byte[15];
+ dst.get(positioned);
+ matchWithInputData(positioned, 50, 15);
+
+ // ... without disturbing the stream's own position ...
+ assertEquals(posBeforePositionedRead, chunkStream.getPos());
+
+ // ... so the sequential read continues where it left off.
+ byte[] seq2 = new byte[10];
+ assertEquals(10, chunkStream.read(seq2, 0, 10));
+ matchWithInputData(seq2, 10, 10);
+ }
+
+ @Test
+ public void testPositionedReadAtEof() throws Exception {
+ ByteBuffer dst = ByteBuffer.allocate(10);
+ assertEquals(-1, chunkStream.readPositioned(CHUNK_SIZE, dst));
+ assertEquals(-1, chunkStream.readPositioned(CHUNK_SIZE + 1, dst));
+ }
+
+ @Test
+ public void testPositionedReadTruncatedAtChunkEnd() throws Exception {
+ // Request more than remains in the chunk; only the remainder is returned.
+ ByteBuffer dst = ByteBuffer.allocate(50);
+ int read = chunkStream.readPositioned(CHUNK_SIZE - 20, dst);
+ assertEquals(20, read);
+ dst.flip();
+ byte[] tail = new byte[20];
+ dst.get(tail);
+ matchWithInputData(tail, CHUNK_SIZE - 20, 20);
+ }
+
+ @Test
+ public void testConcurrentPositionedRead() throws Exception {
+ Checksum checksum = new Checksum(ChecksumType.CRC32, BYTES_PER_CHECKSUM);
+ byte[] largeChunkData = generateRandomData(SOURCE_SIZE);
+ ChunkInfo largeChunkInfo = ChunkInfo.newBuilder()
+ .setChunkName("large-chunk")
+ .setOffset(0)
+ .setLen(SOURCE_SIZE)
+ .setChecksumData(checksum.computeChecksum(
+ largeChunkData, 0, SOURCE_SIZE).getProtoBufMessage())
+ .build();
+ try (DummyChunkInputStream largeStream = new DummyChunkInputStream(
+ largeChunkInfo, blockID, null, false, largeChunkData.clone(), null)) {
+ PositionedReadTestHelper.runConcurrentPositionedReads(largeChunkData,
+ (offset, buf) -> {
+ int read = largeStream.readPositioned(offset, buf);
+ if (read != BUFFER_SIZE) {
+ throw new AssertionError("expected " + BUFFER_SIZE + " bytes at
" + offset
+ + " but read " + read);
+ }
+ });
+ }
+ }
+
@Test
public void testSeek() throws Exception {
seekAndVerify(0);
diff --git
a/hadoop-hdds/client/src/test/java/org/apache/hadoop/hdds/scm/storage/TestMultipartInputStream.java
b/hadoop-hdds/client/src/test/java/org/apache/hadoop/hdds/scm/storage/TestMultipartInputStream.java
new file mode 100644
index 00000000000..59a010c127f
--- /dev/null
+++
b/hadoop-hdds/client/src/test/java/org/apache/hadoop/hdds/scm/storage/TestMultipartInputStream.java
@@ -0,0 +1,126 @@
+/*
+ * 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.hdds.scm.storage;
+
+import static
org.apache.hadoop.hdds.scm.storage.PositionedReadTestHelper.SOURCE_SIZE;
+import static
org.apache.hadoop.hdds.scm.storage.TestChunkInputStream.generateRandomData;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.mock;
+
+import com.google.common.primitives.Bytes;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+import org.apache.hadoop.hdds.client.BlockID;
+import org.apache.hadoop.hdds.client.ContainerBlockID;
+import org.apache.hadoop.hdds.conf.OzoneConfiguration;
+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.scm.OzoneClientConfig;
+import org.apache.hadoop.hdds.scm.pipeline.MockPipeline;
+import org.apache.hadoop.hdds.scm.pipeline.Pipeline;
+import org.apache.hadoop.ozone.common.Checksum;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Tests for {@link MultipartInputStream}'s functionality.
+ */
+public class TestMultipartInputStream {
+
+ private static final int PART_SIZE = SOURCE_SIZE / 2;
+
+ @Test
+ public void testPositionedReadPartialAtEof() throws Exception {
+ int fileLen = 1024;
+ byte[] partData = generateRandomData(fileLen);
+ OzoneConfiguration conf = new OzoneConfiguration();
+ OzoneClientConfig clientConfig = conf.getObject(OzoneClientConfig.class);
+ clientConfig.setChecksumVerify(false);
+ Pipeline pipeline = MockPipeline.createSingleNodePipeline();
+ Function<BlockID, BlockLocationInfo> refreshFunction =
mock(Function.class);
+ Checksum checksum = new Checksum(ChecksumType.NONE, 1024);
+
+ BlockInputStream part = createBlockStream(new BlockID(new
ContainerBlockID(1, 1)),
+ partData, pipeline, refreshFunction, clientConfig, checksum);
+ try (MultipartInputStream multipartStream =
+ new MultipartInputStream("test-key", Collections.singletonList(part)))
{
+ multipartStream.initialize();
+ int position = 12;
+ int expectedBytes = fileLen - position;
+ ByteBuffer buffer = ByteBuffer.allocate(expectedBytes * 2);
+ assertTrue(multipartStream.readFully(position, buffer));
+ assertEquals(expectedBytes, buffer.position());
+ }
+ }
+
+ @Test
+ public void testConcurrentPositionedRead() throws Exception {
+ byte[] part0Data = generateRandomData(PART_SIZE);
+ byte[] part1Data = generateRandomData(PART_SIZE);
+ byte[] keyData = Bytes.concat(part0Data, part1Data);
+
+ OzoneConfiguration conf = new OzoneConfiguration();
+ OzoneClientConfig clientConfig = conf.getObject(OzoneClientConfig.class);
+ clientConfig.setChecksumVerify(false);
+ Pipeline pipeline = MockPipeline.createSingleNodePipeline();
+ Function<BlockID, BlockLocationInfo> refreshFunction =
mock(Function.class);
+ Checksum checksum = new Checksum(ChecksumType.NONE, 1024);
+
+ BlockInputStream part0 = createBlockStream(new BlockID(new
ContainerBlockID(1, 1)),
+ part0Data, pipeline, refreshFunction, clientConfig, checksum);
+ BlockInputStream part1 = createBlockStream(new BlockID(new
ContainerBlockID(1, 2)),
+ part1Data, pipeline, refreshFunction, clientConfig, checksum);
+
+ List<BlockInputStream> parts = new ArrayList<>();
+ parts.add(part0);
+ parts.add(part1);
+ try (MultipartInputStream multipartStream = new
MultipartInputStream("test-key", parts)) {
+ multipartStream.initialize();
+ PositionedReadTestHelper.runConcurrentPositionedReads(keyData,
+ (offset, buf) -> {
+ if (!multipartStream.readFully(offset, buf)) {
+ throw new AssertionError("stateless readFully returned false at
" + offset);
+ }
+ });
+ }
+ }
+
+ private static BlockInputStream createBlockStream(BlockID blockID, byte[]
blockData,
+ Pipeline pipeline, Function<BlockID, BlockLocationInfo> refreshFunction,
+ OzoneClientConfig clientConfig, Checksum checksum) throws Exception {
+ List<ChunkInfo> chunks = new ArrayList<>(1);
+ Map<String, byte[]> chunkDataMap = new HashMap<>();
+ String chunkName = "chunk-" + blockID.getLocalID();
+ ChunkInfo chunkInfo = ChunkInfo.newBuilder()
+ .setChunkName(chunkName)
+ .setOffset(0)
+ .setLen(blockData.length)
+ .setChecksumData(checksum.computeChecksum(
+ blockData, 0, blockData.length).getProtoBufMessage())
+ .build();
+ chunkDataMap.put(chunkName, blockData);
+ chunks.add(chunkInfo);
+ return new DummyBlockInputStream(blockID, blockData.length, pipeline, null,
+ null, refreshFunction, chunks, chunkDataMap, clientConfig);
+ }
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]