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 7a49f649958 HDDS-15794. StreamBlockInputStream.onNext may throw 
IndexOutOfBoundsException on short payloads (#10698)
7a49f649958 is described below

commit 7a49f649958548e2369085089738ff4593a7040f
Author: Siyao Meng <[email protected]>
AuthorDate: Mon Jul 13 23:00:14 2026 -0700

    HDDS-15794. StreamBlockInputStream.onNext may throw 
IndexOutOfBoundsException on short payloads (#10698)
---
 .../hdds/scm/storage/StreamBlockInputStream.java   |   9 +-
 .../scm/storage/TestStreamBlockInputStream.java    | 156 +++++++++++++++++++++
 2 files changed, 162 insertions(+), 3 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 b008abdb56c..c15cd338908 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
@@ -536,15 +536,18 @@ public void 
onNext(ContainerProtos.ContainerCommandResponseProto containerComman
         }
         offerToQueue(readBlock);
       } catch (Exception e) {
+        // Record the failure first: the log and observer calls below must not 
mask it.
+        setFailed(e);
         final ByteString data = readBlock.getData();
         final long offset = readBlock.getOffset();
         final StreamingReadResponse r = getResponse();
         LOG.warn("Failed to process block {} response at offset={}, size={}: 
{}, {}",
             getBlockID().getContainerBlockID(),
-            offset, data.size(), StringUtils.bytes2Hex(data.substring(0, 
10).asReadOnlyByteBuffer()),
+            offset, data.size(), 
StringUtils.bytes2Hex(data.asReadOnlyByteBuffer(), 10),
             readBlock.getChecksumData(), e);
-        setFailed(e);
-        r.getRequestObserver().onError(e);
+        if (r != null) {
+          r.getRequestObserver().onError(e);
+        }
         releaseResources();
       }
     }
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 4483945509b..cded373e7c8 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,9 +17,11 @@
 
 package org.apache.hadoop.hdds.scm.storage;
 
+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.eq;
 import static org.mockito.Mockito.doAnswer;
@@ -31,6 +33,7 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import java.io.IOException;
 import java.nio.ByteBuffer;
 import java.time.Duration;
 import java.util.Collections;
@@ -55,6 +58,7 @@
 import org.apache.hadoop.hdds.scm.XceiverClientGrpc;
 import org.apache.hadoop.hdds.scm.pipeline.Pipeline;
 import org.apache.hadoop.hdds.security.token.OzoneBlockTokenIdentifier;
+import org.apache.hadoop.ozone.common.OzoneChecksumException;
 import org.apache.hadoop.security.token.Token;
 import org.apache.ratis.thirdparty.com.google.protobuf.ByteString;
 import org.apache.ratis.thirdparty.io.grpc.stub.ClientCallStreamObserver;
@@ -604,6 +608,142 @@ public void 
testReadWithoutNewRequestGetsFreshTimeoutBudget() throws Exception {
     }
   }
 
+  /**
+   * The server may end the stream (onCompleted) after a request has been sent 
but before any
+   * data is delivered, e.g. when the datanode shuts down gracefully. poll() 
then returns null
+   * (stream done, queue drained) while readFromQueue() is waiting for data. 
The premature end
+   * of the stream must surface as EOF, not as a NullPointerException that 
bypasses
+   * handleExceptions() retry handling.
+   */
+  @Test
+  public void testStreamCompletedMidReadWithEmptyQueueSurfacesEof() throws 
Exception {
+    OzoneClientConfig clientConfig = newStreamReadConfig();
+    BlockID blockID = new BlockID(1L, 14L);
+    Pipeline pipeline = mockStandalonePipeline();
+    ClientCallStreamObserver<ContainerCommandRequestProto> requestObserver =
+        mock(ClientCallStreamObserver.class);
+    StreamingReadResponse streamingReadResponse = 
mock(StreamingReadResponse.class);
+    
when(streamingReadResponse.getRequestObserver()).thenReturn(requestObserver);
+
+    AtomicReference<StreamingReaderSpi> readerRef = new AtomicReference<>();
+    XceiverClientGrpc xceiverClient = mock(XceiverClientGrpc.class);
+    doAnswer(inv -> {
+      StreamingReaderSpi reader = inv.getArgument(1);
+      reader.setStreamingReadResponse(streamingReadResponse);
+      readerRef.set(reader);
+      return null;
+    }).when(xceiverClient).initStreamRead(any(BlockID.class), any(), any());
+
+    // The server closes the stream without delivering any of the requested 
data, so the
+    // reader ends up polling a drained queue with the future already 
completed.
+    doAnswer(inv -> {
+      readerRef.get().onCompleted();
+      return null;
+    }).when(xceiverClient).streamRead(any(), any());
+
+    XceiverClientFactory xceiverClientFactory = 
mock(XceiverClientFactory.class);
+    when(xceiverClientFactory.acquireClientForReadData(any(Pipeline.class)))
+        .thenReturn(xceiverClient);
+
+    try (StreamBlockInputStream sbis = new StreamBlockInputStream(
+        blockID, 4L, pipeline, null, xceiverClientFactory,
+        NO_REFRESH, clientConfig)) {
+      ByteBuffer buf = ByteBuffer.allocate(4);
+      int bytesRead = assertDoesNotThrow(() -> sbis.read(buf),
+          "premature stream completion must not throw NullPointerException");
+      assertEquals(-1, bytesRead, "premature stream completion should surface 
as EOF");
+      assertEquals(0, buf.position());
+    }
+  }
+
+  /**
+   * A truncated payload fails checksum verification in onNext(). The failure 
handling must
+   * record the real failure via setFailed() and propagate it, even though the 
payload is
+   * shorter than the 10-byte hex preview included in the warning log.
+   */
+  @Test
+  public void testChecksumFailureOnShortPayloadSurfacesRealError() throws 
Exception {
+    OzoneClientConfig clientConfig = newStreamReadConfig();
+    clientConfig.setChecksumVerify(true);
+    BlockID blockID = new BlockID(1L, 15L);
+    byte[] data = {1, 2, 3, 4};
+    Pipeline pipeline = mockStandalonePipeline();
+    ClientCallStreamObserver<ContainerCommandRequestProto> requestObserver =
+        mock(ClientCallStreamObserver.class);
+    StreamingReadResponse streamingReadResponse = 
mock(StreamingReadResponse.class);
+    
when(streamingReadResponse.getRequestObserver()).thenReturn(requestObserver);
+
+    AtomicReference<StreamingReaderSpi> readerRef = new AtomicReference<>();
+    XceiverClientGrpc xceiverClient = mock(XceiverClientGrpc.class);
+    doAnswer(inv -> {
+      StreamingReaderSpi reader = inv.getArgument(1);
+      reader.setStreamingReadResponse(streamingReadResponse);
+      readerRef.set(reader);
+      return null;
+    }).when(xceiverClient).initStreamRead(any(BlockID.class), any(), any());
+
+    // Deliver a payload shorter than 10 bytes whose checksum does not match 
the data.
+    doAnswer(inv -> {
+      readerRef.get().onNext(buildCorruptResponseProto(data, 0));
+      return null;
+    }).when(xceiverClient).streamRead(any(), any());
+
+    XceiverClientFactory xceiverClientFactory = 
mock(XceiverClientFactory.class);
+    when(xceiverClientFactory.acquireClientForReadData(any(Pipeline.class)))
+        .thenReturn(xceiverClient);
+
+    try (StreamBlockInputStream sbis = new StreamBlockInputStream(
+        blockID, data.length, pipeline, null, xceiverClientFactory,
+        NO_REFRESH, clientConfig)) {
+      ByteBuffer buf = ByteBuffer.allocate(data.length);
+      IOException thrown = assertThrows(IOException.class, () -> 
sbis.read(buf),
+          "checksum failure should surface as an IOException");
+      assertThat(thrown).hasRootCauseInstanceOf(OzoneChecksumException.class);
+    }
+    verify(requestObserver, 
times(1)).onError(any(OzoneChecksumException.class));
+  }
+
+  /**
+   * onNext() may fail before XceiverClientGrpc has registered the 
StreamingReadResponse, so
+   * getResponse() is still null while the failure is reported. The real 
failure must still be
+   * recorded and surfaced to the reader.
+   */
+  @Test
+  public void testChecksumFailureBeforeResponseRegistered() throws Exception {
+    OzoneClientConfig clientConfig = newStreamReadConfig();
+    clientConfig.setChecksumVerify(true);
+    BlockID blockID = new BlockID(1L, 16L);
+    byte[] data = {1, 2, 3};
+    Pipeline pipeline = mockStandalonePipeline();
+    ClientCallStreamObserver<ContainerCommandRequestProto> requestObserver =
+        mock(ClientCallStreamObserver.class);
+    StreamingReadResponse streamingReadResponse = 
mock(StreamingReadResponse.class);
+    
when(streamingReadResponse.getRequestObserver()).thenReturn(requestObserver);
+
+    XceiverClientGrpc xceiverClient = mock(XceiverClientGrpc.class);
+    doAnswer(inv -> {
+      StreamingReaderSpi reader = inv.getArgument(1);
+      // The corrupt response arrives before setStreamingReadResponse() has 
been called.
+      reader.onNext(buildCorruptResponseProto(data, 0));
+      reader.setStreamingReadResponse(streamingReadResponse);
+      return null;
+    }).when(xceiverClient).initStreamRead(any(BlockID.class), any(), any());
+
+    XceiverClientFactory xceiverClientFactory = 
mock(XceiverClientFactory.class);
+    when(xceiverClientFactory.acquireClientForReadData(any(Pipeline.class)))
+        .thenReturn(xceiverClient);
+
+    try (StreamBlockInputStream sbis = new StreamBlockInputStream(
+        blockID, data.length, pipeline, null, xceiverClientFactory,
+        NO_REFRESH, clientConfig)) {
+      ByteBuffer buf = ByteBuffer.allocate(data.length);
+      IOException thrown = assertThrows(IOException.class, () -> 
sbis.read(buf),
+          "checksum failure should surface as an IOException");
+      assertThat(thrown).hasRootCauseInstanceOf(OzoneChecksumException.class);
+    }
+    verify(requestObserver, never()).onError(any());
+  }
+
   private ReadBlockResponseProto buildReadBlockResponse(byte[] data) {
     return ReadBlockResponseProto.newBuilder()
         .setOffset(0)
@@ -629,4 +769,20 @@ private ContainerCommandResponseProto 
buildResponseProto(byte[] data, long offse
             .build())
         .build();
   }
+
+  private ContainerCommandResponseProto buildCorruptResponseProto(byte[] data, 
long offset) {
+    return ContainerCommandResponseProto.newBuilder()
+        .setCmdType(Type.ReadBlock)
+        .setResult(ContainerProtos.Result.SUCCESS)
+        .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())
+            .build())
+        .build();
+  }
 }


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to