This is an automated email from the ASF dual-hosted git repository.
peterxcli 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 d5d4df4d4b6 HDDS-16395. Add lock-free StreamBlock pread (#11314)
d5d4df4d4b6 is described below
commit d5d4df4d4b625a645ed406b2334fbb15d17342eb
Author: Tak Lon (Stephen) Wu <[email protected]>
AuthorDate: Wed Sep 30 20:37:21 2026 -0700
HDDS-16395. Add lock-free StreamBlock pread (#11314)
---
.../hadoop/hdds/scm/storage/BlockInputStream.java | 6 +-
.../hdds/scm/storage/MultipartInputStream.java | 48 +----
.../hadoop/hdds/scm/storage/PartInputStream.java | 13 ++
.../hdds/scm/storage/StreamBlockInputStream.java | 159 ++++++++++++++---
.../hdds/scm/storage/TestMultipartInputStream.java | 69 +++++++
.../scm/storage/TestStreamBlockInputStream.java | 198 +++++++++++++++++++++
.../hadoop/hdds/utils/ConnectionFailureUtils.java | 2 +-
.../hdds/utils/TestConnectionFailureUtils.java | 14 ++
.../ozone/client/io/OzoneCryptoInputStream.java | 5 +
9 files changed, 439 insertions(+), 75 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 57757cb79f7..c3b1f3ed73f 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
@@ -473,7 +473,11 @@ protected synchronized int
readWithStrategy(ByteReaderStrategy strategy)
*
* @return bytes copied into {@code dst}, or {@link #EOF} at EOF
*/
- int readPositioned(long blockRelativePosition, ByteBuffer dst) throws
IOException {
+ @Override
+ public int readPositioned(long blockRelativePosition, ByteBuffer dst) throws
IOException {
+ if (!dst.hasRemaining()) {
+ return EOF;
+ }
if (blockRelativePosition < 0) {
return EOF;
}
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 584d97c778e..00a119690b0 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
@@ -196,58 +196,19 @@ public synchronized void seek(long pos) throws
IOException {
@Override
public boolean readFully(long position, ByteBuffer buffer) throws
IOException {
- if (isStreamBlockInputStream) {
- return readFullyStreamBlock(position, buffer);
+ if (!statelessPositionedReadSupported) {
+ return false;
}
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 {
- int remainingBeforeRead = buffer.remaining();
- if (remainingBeforeRead == 0) {
- return true;
- }
- read(new ByteBufferReader(buffer) {
- @Override
- int readImpl(InputStream inputStream) throws IOException {
- return Preconditions.assertInstanceOf(inputStream,
StreamBlockInputStream.class)
- .readFully(getBuffer(), false);
- }
- });
- if (remainingBeforeRead - buffer.remaining() == 0) {
- throw new EOFException("EOF encountered at pos: " + position +
- " for key: " + key);
- }
- } finally {
- seek(oldPos);
- }
- 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.
+ * Shared stateless positioned-read loop across parts using {@link
PartInputStream#readPositioned}.
*/
private boolean readFullyStateless(long position, ByteBuffer buffer) throws
IOException {
if (!buffer.hasRemaining()) {
return true;
}
- if (!statelessPositionedReadSupported) {
- return false;
- }
long pos = position;
int bytesRead = 0;
@@ -260,9 +221,8 @@ private boolean readFullyStateless(long position,
ByteBuffer buffer) throws IOEx
" 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);
+ int n = partStreams.get(idx).readPositioned(partPos, buffer);
if (n <= 0) {
if (bytesRead > 0) {
return true;
diff --git
a/hadoop-hdds/client/src/main/java/org/apache/hadoop/hdds/scm/storage/PartInputStream.java
b/hadoop-hdds/client/src/main/java/org/apache/hadoop/hdds/scm/storage/PartInputStream.java
index 7aa0d594c38..3d7ee44c58e 100644
---
a/hadoop-hdds/client/src/main/java/org/apache/hadoop/hdds/scm/storage/PartInputStream.java
+++
b/hadoop-hdds/client/src/main/java/org/apache/hadoop/hdds/scm/storage/PartInputStream.java
@@ -18,6 +18,7 @@
package org.apache.hadoop.hdds.scm.storage;
import java.io.IOException;
+import java.nio.ByteBuffer;
import org.apache.hadoop.fs.CanUnbuffer;
import org.apache.hadoop.fs.Seekable;
@@ -33,4 +34,16 @@ default long getRemaining() throws IOException {
}
void close() throws IOException;
+
+ /**
+ * Positioned read within this part starting at {@code partOffset}.
+ *
+ * @return bytes copied into {@code buffer}, {@code -1} if {@code buffer}
has no remaining space
+ * or at EOF
+ * @throws UnsupportedOperationException if this part stream does not
support positioned read
+ */
+ default int readPositioned(long partOffset, ByteBuffer buffer) throws
IOException {
+ throw new UnsupportedOperationException(
+ "Positioned read is not supported by " + getClass().getName());
+ }
}
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 ddef8e54f42..ce587f37292 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
@@ -23,10 +23,10 @@
import java.io.IOException;
import java.nio.ByteBuffer;
import java.time.Duration;
-import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
@@ -58,6 +58,7 @@
import org.apache.hadoop.security.token.Token;
import org.apache.ratis.protocol.exceptions.TimeoutIOException;
import org.apache.ratis.thirdparty.com.google.protobuf.ByteString;
+import org.apache.ratis.thirdparty.io.grpc.Status;
import org.apache.ratis.thirdparty.io.grpc.StatusRuntimeException;
import org.apache.ratis.thirdparty.io.grpc.stub.ClientCallStreamObserver;
import org.apache.ratis.util.Preconditions;
@@ -96,7 +97,8 @@ public class StreamBlockInputStream extends
BlockExtendedInputStream {
private final Function<BlockID, BlockLocationInfo> refreshFunction;
private final RetryPolicy retryPolicy;
private int retries = 0;
- private final Set<DatanodeID> failedStreamingDatanodes = new HashSet<>();
+ // Shared by sequential and positioned reads; must be thread-safe for
concurrent access.
+ private final Set<DatanodeID> failedStreamingDatanodes =
ConcurrentHashMap.newKeySet();
public StreamBlockInputStream(
BlockID blockID, long length, Pipeline pipeline,
@@ -178,6 +180,96 @@ synchronized int readFully(ByteBuffer targetBuf, boolean
preRead) throws IOExcep
return read > 0 ? read : EOF;
}
+ /**
+ * Positioned read served by a dedicated one-shot streaming read which never
touches the
+ * cursor, the pre-read buffer or the sequential reader.
+ *
+ * @param blockOffset the offset within the block to read from.
+ * @param dst the buffer to read into.
+ * @return the number of bytes copied into {@code dst}, or -1 if no byte
could be read.
+ */
+ @Override
+ public int readPositioned(long blockOffset, ByteBuffer dst) throws
IOException {
+ if (!dst.hasRemaining()) {
+ return EOF;
+ }
+ if (blockOffset < 0 || blockOffset >= blockLength) {
+ return EOF;
+ }
+ final XceiverClientFactory factory;
+ synchronized (this) {
+ checkOpen();
+ factory = xceiverClientFactory;
+ }
+ final int length = Math.toIntExact(Math.min(dst.remaining(), blockLength -
blockOffset));
+ final int startPosition = dst.position();
+ int callRetries = 0;
+ while (true) {
+ final Pipeline pipeline = pipelineRef.get();
+ final XceiverClientGrpc client = acquireGrpcClientForReadData(factory,
pipeline);
+ final StreamingReader reader = new StreamingReader(client);
+ try {
+ // On failure initStreamRead releases its own permit, so the reader is
closed only after it succeeds.
+ client.initStreamRead(blockID, reader, failedStreamingDatanodes);
+ try {
+ return preadOnce(client, reader, pipeline, blockOffset, length, dst);
+ } finally {
+ closeStream(reader);
+ }
+ } catch (IOException e) {
+ dst.position(startPosition);
+ handlePreadException(e, reader, blockOffset, callRetries++);
+ } finally {
+ factory.releaseClientForReadData(client, false);
+ }
+ }
+ }
+
+ private int preadOnce(XceiverClientGrpc client, StreamingReader reader,
Pipeline pipeline,
+ long blockOffset, int length, ByteBuffer dst) throws IOException {
+ final StreamingReadResponse response = reader.getResponse();
+ if (response == null) {
+ throw new IOException("Uninitialized StreamingReadResponse: " + blockID);
+ }
+ client.streamRead(ContainerProtocolCalls.buildReadBlockCommandProto(
+ blockID, blockOffset, length, responseDataSize, tokenRef.get(),
pipeline), response);
+
+ int copied = 0;
+ while (copied < length) {
+ final ReadBlockResponseProto proto = reader.poll();
+ if (proto == null) {
+ break;
+ }
+ final ByteBuffer buffer = getByteBuffer(proto, blockOffset + copied);
+ if (buffer == null || !buffer.hasRemaining()) {
+ continue;
+ }
+ buffer.limit(buffer.position() + Math.min(buffer.remaining(), length -
copied));
+ copied += buffer.remaining();
+ dst.put(buffer);
+ }
+ return copied > 0 ? copied : EOF;
+ }
+
+ private void handlePreadException(IOException cause, StreamingReader reader,
long blockOffset,
+ int callRetries) throws IOException {
+ final IOException root = ConnectionFailureUtils.unwrapCause(cause);
+ if (Status.fromThrowable(cause).getCode() == Status.Code.OUT_OF_RANGE) {
+ final EOFException eof = new EOFException("Failed to read block " +
blockID + " at offset "
+ + blockOffset + ": " + cause.getMessage());
+ eof.initCause(cause);
+ throw eof;
+ }
+ final boolean retriable = root instanceof StorageContainerException ||
isConnectivityIssue(root)
+ || root instanceof TimeoutIOException;
+ if (!retriable || !shouldRetryRead(root, retryPolicy, callRetries)) {
+ throw cause;
+ }
+ recordFailedStreamingDatanode(reader);
+ refreshBlockInfo(root);
+ LOG.warn("Refreshing block data to pread block {} due to {}", blockID,
cause.getMessage());
+ }
+
private synchronized boolean dataAvailableToRead(int length, boolean
preRead) throws IOException {
if (position >= blockLength) {
return false;
@@ -285,7 +377,10 @@ private synchronized void closeReader(String reason) {
final StreamingReader reader = streamingReader;
streamingReader = null;
LOG.debug("{} closeReader for {}", getName(reader), reason);
+ closeStream(reader);
+ }
+ private static void closeStream(StreamingReader reader) {
reader.onCompleted();
final StreamingReadResponse response = reader.getResponse();
@@ -314,31 +409,34 @@ protected synchronized void checkOpen() throws
IOException {
protected synchronized void acquireClient() throws IOException {
checkOpen();
if (xceiverClient == null) {
- final Pipeline pipeline = pipelineRef.get();
- final XceiverClientSpi client;
- try {
- client = xceiverClientFactory.acquireClientForReadData(pipeline);
- } catch (IOException ioe) {
- LOG.warn("Failed to acquire client for pipeline {}, block {}",
pipeline, blockID);
- throw ioe;
- }
-
- if (client == null) {
- throw new IOException("Failed to acquire client for " + pipeline);
- }
- if (!(client instanceof XceiverClientGrpc)) {
- throw new IOException("Unexpected client class: " +
client.getClass().getName() + ", " + pipeline);
- }
+ xceiverClient = acquireGrpcClientForReadData(xceiverClientFactory,
pipelineRef.get());
+ }
+ }
- xceiverClient = (XceiverClientGrpc) client;
+ private XceiverClientGrpc acquireGrpcClientForReadData(XceiverClientFactory
factory, Pipeline pipeline)
+ throws IOException {
+ final XceiverClientSpi acquired;
+ try {
+ acquired = factory.acquireClientForReadData(pipeline);
+ } catch (IOException ioe) {
+ LOG.warn("Failed to acquire client for pipeline {}, block {}", pipeline,
blockID);
+ throw ioe;
+ }
+ if (acquired == null) {
+ throw new IOException("Failed to acquire client for " + pipeline);
+ }
+ if (!(acquired instanceof XceiverClientGrpc)) {
+ factory.releaseClientForReadData(acquired, false);
+ throw new IOException("Unexpected client class: " +
acquired.getClass().getName() + ", " + pipeline);
}
+ return (XceiverClientGrpc) acquired;
}
private synchronized void initialize() throws IOException {
while (streamingReader == null) {
try {
acquireClient();
- final StreamingReader reader = new StreamingReader();
+ final StreamingReader reader = new StreamingReader(xceiverClient);
LOG.debug("{}: new StreamingReader", getName(reader));
xceiverClient.initStreamRead(blockID, reader,
failedStreamingDatanodes);
streamingReader = reader;
@@ -394,10 +492,14 @@ private void handleExceptions(IOException cause) throws
IOException {
}
private void recordFailedStreamingDatanode() {
- if (streamingReader == null) {
+ recordFailedStreamingDatanode(streamingReader);
+ }
+
+ private void recordFailedStreamingDatanode(StreamingReader reader) {
+ if (reader == null) {
return;
}
- final StreamingReadResponse response = streamingReader.getResponse();
+ final StreamingReadResponse response = reader.getResponse();
if (response == null) {
return;
}
@@ -426,12 +528,6 @@ private void refreshBlockInfo(IOException cause) throws
IOException {
refreshBlockInfo(cause, blockID, pipelineRef, tokenRef, refreshFunction);
}
- private synchronized void releaseStreamResources() {
- if (xceiverClient != null) {
- xceiverClient.completeStreamRead();
- }
- }
-
@Override
public String toString() {
return name;
@@ -480,10 +576,11 @@ public String toString() {
}
/**
- * Implementation of a StreamObserver used to received and buffer streaming
GRPC reads.
+ * StreamObserver used for sequential reads and for one-shot positioned
reads.
*/
public class StreamingReader implements StreamingReaderSpi {
private final String name = StreamBlockInputStream.this.name + "-reader" +
READER_ID.getAndIncrement();
+ private final XceiverClientGrpc client;
/** Response queue: poll is blocking while offer is non-blocking. */
private final BlockingQueue<ReadBlockResponseProto> responseQueue = new
LinkedBlockingQueue<>();
@@ -492,6 +589,10 @@ public class StreamingReader implements StreamingReaderSpi
{
private final AtomicBoolean semaphoreReleased = new AtomicBoolean(false);
private final AtomicReference<StreamingReadResponse> response = new
AtomicReference<>();
+ StreamingReader(XceiverClientGrpc client) {
+ this.client = client;
+ }
+
void checkError() throws IOException {
if (future.isCompletedExceptionally()) {
try {
@@ -569,7 +670,7 @@ private ReadBuffer read(int length, boolean preRead) throws
IOException {
private void releaseResources() {
if (semaphoreReleased.compareAndSet(false, true)) {
- releaseStreamResources();
+ client.completeStreamRead();
}
}
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
index 59a010c127f..7ac64376390 100644
---
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
@@ -21,7 +21,13 @@
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.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
import com.google.common.primitives.Bytes;
import java.nio.ByteBuffer;
@@ -41,6 +47,7 @@
import org.apache.hadoop.hdds.scm.pipeline.Pipeline;
import org.apache.hadoop.ozone.common.Checksum;
import org.junit.jupiter.api.Test;
+import org.mockito.invocation.InvocationOnMock;
/**
* Tests for {@link MultipartInputStream}'s functionality.
@@ -73,6 +80,49 @@ public void testPositionedReadPartialAtEof() throws
Exception {
}
}
+ @Test
+ public void testStreamBlockPositionedReadPartialAtEof() throws Exception {
+ int fileLen = 1024;
+ byte[] partData = generateRandomData(fileLen);
+ StreamBlockInputStream part = stubStreamBlockPart(partData);
+ try (MultipartInputStream multipartStream =
+ new MultipartInputStream("test-key", Collections.singletonList(part)))
{
+ multipartStream.initialize();
+ assertTrue(multipartStream.isStreamBlockInputStream());
+ int position = 12;
+ int expectedBytes = fileLen - position;
+ ByteBuffer buffer = ByteBuffer.allocate(expectedBytes * 2);
+ assertTrue(multipartStream.readFully(position, buffer));
+ assertEquals(expectedBytes, buffer.position());
+ verify(part, never()).seek(anyLong());
+ }
+ }
+
+ @Test
+ public void testStreamBlockConcurrentPositionedRead() throws Exception {
+ byte[] part0Data = generateRandomData(PART_SIZE);
+ byte[] part1Data = generateRandomData(PART_SIZE);
+ byte[] keyData = Bytes.concat(part0Data, part1Data);
+
+ StreamBlockInputStream part0 = stubStreamBlockPart(part0Data);
+ StreamBlockInputStream part1 = stubStreamBlockPart(part1Data);
+ List<StreamBlockInputStream> parts = new ArrayList<>();
+ parts.add(part0);
+ parts.add(part1);
+ try (MultipartInputStream multipartStream = new
MultipartInputStream("test-key", parts)) {
+ multipartStream.initialize();
+ assertTrue(multipartStream.isStreamBlockInputStream());
+ PositionedReadTestHelper.runConcurrentPositionedReads(keyData,
+ (offset, buf) -> {
+ if (!multipartStream.readFully(offset, buf)) {
+ throw new AssertionError("stateless readFully returned false at
" + offset);
+ }
+ });
+ verify(part0, never()).seek(anyLong());
+ verify(part1, never()).seek(anyLong());
+ }
+ }
+
@Test
public void testConcurrentPositionedRead() throws Exception {
byte[] part0Data = generateRandomData(PART_SIZE);
@@ -105,6 +155,25 @@ public void testConcurrentPositionedRead() throws
Exception {
}
}
+ private static StreamBlockInputStream stubStreamBlockPart(byte[] partData)
throws Exception {
+ StreamBlockInputStream part = mock(StreamBlockInputStream.class);
+ when(part.getLength()).thenReturn((long) partData.length);
+ doAnswer(invocation -> readPositionedFrom(partData, invocation)).when(part)
+ .readPositioned(anyLong(), any(ByteBuffer.class));
+ return part;
+ }
+
+ private static int readPositionedFrom(byte[] data, InvocationOnMock
invocation) {
+ long offset = invocation.getArgument(0);
+ ByteBuffer dst = invocation.getArgument(1);
+ if (offset < 0 || offset >= data.length) {
+ return -1;
+ }
+ int toCopy = Math.min(dst.remaining(), data.length - (int) offset);
+ dst.put(data, (int) offset, toCopy);
+ return toCopy;
+ }
+
private static BlockInputStream createBlockStream(BlockID blockID, byte[]
blockData,
Pipeline pipeline, Function<BlockID, BlockLocationInfo> refreshFunction,
OzoneClientConfig clientConfig, Checksum checksum) throws Exception {
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 98398326381..88258bfe547 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
@@ -17,12 +17,15 @@
package org.apache.hadoop.hdds.scm.storage;
+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.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doNothing;
@@ -36,7 +39,10 @@
import java.io.IOException;
import java.nio.ByteBuffer;
import java.time.Duration;
+import java.util.Arrays;
import java.util.Collections;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
@@ -107,6 +113,87 @@ public void testCustomStreamReadConfigIsApplied() throws
Exception {
}
}
+ @Test
+ public void testPositionedReadIsStateless() throws Exception {
+ byte[] data = new byte[100];
+ for (int i = 0; i < data.length; i++) {
+ data[i] = (byte) i;
+ }
+ try (StreamBlockInputStream sbis = newStreamForData(data)) {
+ byte[] seq = new byte[10];
+ assertEquals(10, sbis.read(seq, 0, 10));
+ assertArrayEquals(Arrays.copyOfRange(data, 0, 10), seq);
+ long posBeforePositionedRead = sbis.getPos();
+ assertEquals(10, posBeforePositionedRead);
+
+ ByteBuffer dst = ByteBuffer.allocate(15);
+ int read = sbis.readPositioned(50, dst);
+ assertEquals(15, read);
+ dst.flip();
+ byte[] positioned = new byte[15];
+ dst.get(positioned);
+ assertArrayEquals(Arrays.copyOfRange(data, 50, 65), positioned);
+ assertEquals(posBeforePositionedRead, sbis.getPos());
+
+ byte[] seq2 = new byte[10];
+ assertEquals(10, sbis.read(seq2, 0, 10));
+ assertArrayEquals(Arrays.copyOfRange(data, 10, 20), seq2);
+ }
+ }
+
+ @Test
+ public void testPositionedReadAtEof() throws Exception {
+ byte[] data = new byte[100];
+ try (StreamBlockInputStream sbis = newStreamForData(data)) {
+ ByteBuffer dst = ByteBuffer.allocate(10);
+ assertEquals(-1, sbis.readPositioned(data.length, dst));
+ assertEquals(-1, sbis.readPositioned(data.length + 1, dst));
+ // An empty buffer also gets -1, as PartInputStream#readPositioned
documents.
+ assertEquals(-1, sbis.readPositioned(0, ByteBuffer.allocate(0)));
+ }
+ }
+
+ @Test
+ public void testPositionedReadDoesNotReleasePermitWhenInitStreamReadFails()
throws Exception {
+ OzoneClientConfig clientConfig = newStreamReadConfig();
+ clientConfig.setMaxReadRetryCount(0);
+ BlockID blockID = new BlockID(1L, 18L);
+ Pipeline pipeline = mockStandalonePipeline();
+
+ // Like XceiverClientGrpc, fail before the gRPC call starts:
initStreamRead then releases its own permit.
+ XceiverClientGrpc xceiverClient = mock(XceiverClientGrpc.class);
+ doThrow(new IOException("initStreamRead failed"))
+ .when(xceiverClient).initStreamRead(any(BlockID.class), any(), any());
+
+ XceiverClientFactory xceiverClientFactory =
mock(XceiverClientFactory.class);
+ when(xceiverClientFactory.acquireClientForReadData(any(Pipeline.class)))
+ .thenReturn(xceiverClient);
+ doNothing().when(xceiverClientFactory).releaseClientForReadData(any(),
anyBoolean());
+
+ try (StreamBlockInputStream sbis = new StreamBlockInputStream(
+ blockID, 64L, pipeline, null, xceiverClientFactory, NO_REFRESH,
clientConfig)) {
+ ByteBuffer dst = ByteBuffer.allocate(16);
+ assertThrows(IOException.class, () -> sbis.readPositioned(0, dst));
+ }
+
+ verify(xceiverClient, never()).completeStreamRead();
+ }
+
+ @Test
+ public void testConcurrentPositionedRead() throws Exception {
+ byte[] data = TestChunkInputStream.generateRandomData(SOURCE_SIZE);
+ try (StreamBlockInputStream sbis = newStreamForData(data)) {
+ PositionedReadTestHelper.runConcurrentPositionedReads(data,
+ (offset, buf) -> {
+ int read = sbis.readPositioned(offset, buf);
+ if (read != BUFFER_SIZE) {
+ throw new AssertionError("expected " + BUFFER_SIZE + " bytes at
" + offset
+ + " but read " + read);
+ }
+ });
+ }
+ }
+
@Test
public void testReleasesStreamPermitAtBlockEof() throws Exception {
OzoneClientConfig clientConfig = newStreamReadConfig();
@@ -270,6 +357,55 @@ public void
testPollDoesNotDropQueuedItemWhenFutureCompletesFirst() throws Excep
}
}
+ private StreamBlockInputStream newStreamForData(byte[] data) throws
Exception {
+ BlockID blockID = new BlockID(1L, 17L);
+ Pipeline pipeline = mockStandalonePipeline();
+ ClientCallStreamObserver<ContainerCommandRequestProto> requestObserver =
+ mock(ClientCallStreamObserver.class);
+ XceiverClientGrpc xceiverClient = mockOffsetAwareStreamingReadClient(data,
requestObserver);
+ XceiverClientFactory xceiverClientFactory =
mock(XceiverClientFactory.class);
+ when(xceiverClientFactory.acquireClientForReadData(any(Pipeline.class)))
+ .thenReturn(xceiverClient);
+ doNothing().when(xceiverClientFactory).releaseClientForReadData(any(),
anyBoolean());
+ return new StreamBlockInputStream(blockID, data.length, pipeline, null,
+ xceiverClientFactory, NO_REFRESH, newStreamReadConfig());
+ }
+
+ private XceiverClientGrpc mockOffsetAwareStreamingReadClient(byte[] data,
+ ClientCallStreamObserver<ContainerCommandRequestProto> requestObserver)
throws Exception {
+ XceiverClientGrpc xceiverClient = mock(XceiverClientGrpc.class);
+ Map<StreamingReadResponse, StreamingReaderSpi> readersByResponse = new
ConcurrentHashMap<>();
+ doAnswer(invocation -> {
+ StreamingReaderSpi reader = invocation.getArgument(1);
+ StreamingReadResponse response = mock(StreamingReadResponse.class);
+ when(response.getRequestObserver()).thenReturn(requestObserver);
+ reader.setStreamingReadResponse(response);
+ readersByResponse.put(response, reader);
+ return null;
+ }).when(xceiverClient).initStreamRead(any(BlockID.class), any(), any());
+ doAnswer(invocation -> {
+ ContainerCommandRequestProto request = invocation.getArgument(0);
+ StreamingReadResponse response = invocation.getArgument(1);
+ StreamingReaderSpi reader = readersByResponse.get(response);
+ long offset = request.getReadBlock().getOffset();
+ long length = request.getReadBlock().getLength();
+ int start = (int) offset;
+ int end = (int) Math.min(data.length, offset + length);
+ if (start >= data.length) {
+ reader.onCompleted();
+ return null;
+ }
+ byte[] slice = Arrays.copyOfRange(data, start, end);
+ reader.onNext(buildResponseProto(slice, offset));
+ if (end >= data.length) {
+ reader.onCompleted();
+ }
+ return null;
+ }).when(xceiverClient).streamRead(any(ContainerCommandRequestProto.class),
+ any(StreamingReadResponse.class));
+ return xceiverClient;
+ }
+
private OzoneClientConfig newStreamReadConfig() {
OzoneClientConfig clientConfig = new OzoneClientConfig();
clientConfig.setChecksumVerify(false);
@@ -822,6 +958,68 @@ public void testFailFastOnGrpcOutOfRangeStatus() throws
Exception {
}
}
+ /**
+ * A streamed error response (e.g. CONTAINER_NOT_FOUND) is retried after
refreshing the block location,
+ * for both sequential and positioned reads.
+ */
+ @Test
+ public void testRetriesErrorResponseAfterRefresh() throws Exception {
+ OzoneClientConfig clientConfig = newStreamReadConfig();
+ clientConfig.setMaxReadRetryCount(1);
+ clientConfig.setReadRetryInterval(0);
+ byte[] data = new byte[] {1, 2, 3, 4};
+ ClientCallStreamObserver<ContainerCommandRequestProto> requestObserver =
mock(ClientCallStreamObserver.class);
+ StreamingReadResponse streamingReadResponse =
mock(StreamingReadResponse.class);
+
when(streamingReadResponse.getRequestObserver()).thenReturn(requestObserver);
+
when(streamingReadResponse.getDatanodeDetails()).thenReturn(MockDatanodeDetails.randomDatanodeDetails());
+
+ // Every other ReadBlock request gets an error response; only one reader
is active at a time here.
+ AtomicInteger streamReads = new AtomicInteger();
+ AtomicReference<StreamingReaderSpi> lastReader = new AtomicReference<>();
+ XceiverClientGrpc xceiverClient = mock(XceiverClientGrpc.class);
+ doAnswer(inv -> {
+ StreamingReaderSpi reader = inv.getArgument(1);
+ reader.setStreamingReadResponse(streamingReadResponse);
+ lastReader.set(reader);
+ return null;
+ }).when(xceiverClient).initStreamRead(any(BlockID.class), any(), any());
+ doAnswer(inv -> {
+ StreamingReaderSpi reader = lastReader.get();
+ if (streamReads.getAndIncrement() % 2 == 0) {
+ reader.onNext(ContainerCommandResponseProto.newBuilder()
+ .setCmdType(Type.ReadBlock)
+ .setResult(ContainerProtos.Result.CONTAINER_NOT_FOUND)
+ .setMessage("Container not found")
+ .build());
+ } else {
+ reader.onNext(buildResponseProto(data, 0));
+ reader.onCompleted();
+ }
+ return null;
+ }).when(xceiverClient).streamRead(any(), any());
+ XceiverClientFactory xceiverClientFactory =
mock(XceiverClientFactory.class);
+
when(xceiverClientFactory.acquireClientForReadData(any(Pipeline.class))).thenReturn(xceiverClient);
+ AtomicInteger refreshes = new AtomicInteger();
+ Function<BlockID, BlockLocationInfo> refresh = b -> {
+ refreshes.incrementAndGet();
+ return null;
+ };
+
+ try (StreamBlockInputStream sbis = new StreamBlockInputStream(
+ new BlockID(1L, 22L), data.length, mockStandalonePipeline(), null,
xceiverClientFactory,
+ refresh, clientConfig)) {
+ ByteBuffer positioned = ByteBuffer.allocate(data.length);
+ assertEquals(data.length, sbis.readPositioned(0, positioned));
+ assertArrayEquals(data, positioned.array());
+ assertEquals(1, refreshes.get());
+
+ ByteBuffer sequential = ByteBuffer.allocate(data.length);
+ assertEquals(data.length, sbis.read(sequential));
+ assertArrayEquals(data, sequential.array());
+ assertEquals(2, refreshes.get());
+ }
+ }
+
/**
* Mocks a streaming read client which captures the StreamingReaderSpi during
* initStreamRead and drives the given callback when a ReadBlock request is
sent.
diff --git
a/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/utils/ConnectionFailureUtils.java
b/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/utils/ConnectionFailureUtils.java
index d7f45b1cd7e..9ba515e91a2 100644
---
a/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/utils/ConnectionFailureUtils.java
+++
b/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/utils/ConnectionFailureUtils.java
@@ -120,7 +120,7 @@ public static IOException unwrapCause(IOException ex) {
t = t.getCause();
continue;
}
- if (t.getCause() instanceof IOException) {
+ if (t.getCause() instanceof IOException || t.getCause() instanceof
ExecutionException) {
t = t.getCause();
continue;
}
diff --git
a/hadoop-hdds/common/src/test/java/org/apache/hadoop/hdds/utils/TestConnectionFailureUtils.java
b/hadoop-hdds/common/src/test/java/org/apache/hadoop/hdds/utils/TestConnectionFailureUtils.java
index 00c327f6899..ca8f46570be 100644
---
a/hadoop-hdds/common/src/test/java/org/apache/hadoop/hdds/utils/TestConnectionFailureUtils.java
+++
b/hadoop-hdds/common/src/test/java/org/apache/hadoop/hdds/utils/TestConnectionFailureUtils.java
@@ -18,6 +18,7 @@
package org.apache.hadoop.hdds.utils;
import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.io.EOFException;
@@ -27,7 +28,10 @@
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
+import java.util.concurrent.ExecutionException;
import java.util.stream.Stream;
+import org.apache.hadoop.hdds.protocol.datanode.proto.ContainerProtos.Result;
+import
org.apache.hadoop.hdds.scm.container.common.helpers.StorageContainerException;
import org.apache.hadoop.security.AccessControlException;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
@@ -117,6 +121,16 @@ public void testNullIsNotAConnectionFailure() {
assertFalse(ConnectionFailureUtils.isConnectionFailure(null));
}
+ /**
+ * The shape StreamBlockInputStream's reader throws for a failed stream.
+ */
+ @Test
+ public void testUnwrapCauseThroughIOExceptionWrappingExecutionException() {
+ StorageContainerException sce = new StorageContainerException("not found",
Result.CONTAINER_NOT_FOUND);
+ IOException wrapped = new IOException("Streaming read failed", new
ExecutionException(sce));
+ assertSame(sce, ConnectionFailureUtils.unwrapCause(wrapped));
+ }
+
/**
* {@code Throwable.initCause} contractually rejects setting cause to
* the throwable itself, but cycles of length 2+ have appeared in
diff --git
a/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/io/OzoneCryptoInputStream.java
b/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/io/OzoneCryptoInputStream.java
index ab8617e30f4..705aa84d637 100644
---
a/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/io/OzoneCryptoInputStream.java
+++
b/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/io/OzoneCryptoInputStream.java
@@ -188,6 +188,11 @@ public synchronized int available() throws IOException {
return super.available();
}
+ @Override
+ public synchronized int readPositioned(long partOffset, ByteBuffer buffer)
throws IOException {
+ return read(partOffset, buffer);
+ }
+
/**
* Positioned read. Decryption can only happen at Crypto buffer boundaries,
so this stream cannot read
* at an arbitrary position without moving its cursor. The read is
serialized against the other reads:
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]