This is an automated email from the ASF dual-hosted git repository.
adoroszlai 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 f2c7b6fd296 HDDS-14724. Fix infinite CPU spin loop in
ECBlockInputStream (#9833)
f2c7b6fd296 is described below
commit f2c7b6fd296e23900e192c6165101834ff70bfea
Author: wuya <[email protected]>
AuthorDate: Tue Mar 3 23:10:45 2026 +0800
HDDS-14724. Fix infinite CPU spin loop in ECBlockInputStream (#9833)
---
.../hadoop/ozone/client/io/ECBlockInputStream.java | 6 +++
.../ozone/client/io/TestECBlockInputStream.java | 53 ++++++++++++++++++++++
2 files changed, 59 insertions(+)
diff --git
a/hadoop-hdds/client/src/main/java/org/apache/hadoop/ozone/client/io/ECBlockInputStream.java
b/hadoop-hdds/client/src/main/java/org/apache/hadoop/ozone/client/io/ECBlockInputStream.java
index 224e80aaa12..eb876642bef 100644
---
a/hadoop-hdds/client/src/main/java/org/apache/hadoop/ozone/client/io/ECBlockInputStream.java
+++
b/hadoop-hdds/client/src/main/java/org/apache/hadoop/ozone/client/io/ECBlockInputStream.java
@@ -435,6 +435,12 @@ private int readFromStream(BlockExtendedInputStream stream,
+ " from blockGroup " + stream.getBlockID() + " index "
+ currentStreamIndex() + 1);
}
+
+ if (actualRead != expectedRead) {
+ throw new IOException(String.format(
+ "Inconsistent read for blockID=%s index=%d expectedRead=%d
actualRead=%d",
+ stream.getBlockID(), currentStreamIndex() + 1, expectedRead,
actualRead));
+ }
return actualRead;
}
diff --git
a/hadoop-hdds/client/src/test/java/org/apache/hadoop/ozone/client/io/TestECBlockInputStream.java
b/hadoop-hdds/client/src/test/java/org/apache/hadoop/ozone/client/io/TestECBlockInputStream.java
index 68fd37221f0..c0a2985b47d 100644
---
a/hadoop-hdds/client/src/test/java/org/apache/hadoop/ozone/client/io/TestECBlockInputStream.java
+++
b/hadoop-hdds/client/src/test/java/org/apache/hadoop/ozone/client/io/TestECBlockInputStream.java
@@ -31,6 +31,7 @@
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
+import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import org.apache.hadoop.hdds.client.BlockID;
import org.apache.hadoop.hdds.client.ECReplicationConfig;
@@ -50,6 +51,7 @@
import org.apache.hadoop.security.token.Token;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.Timeout;
/**
* Tests for ECBlockInputStream.
@@ -554,6 +556,48 @@ public void testEcPipelineRefreshFunction() {
}
}
+ @Test
+ @Timeout(value = 5, unit = TimeUnit.SECONDS)
+ public void testZeroByteReadThrowsBadDataLocationException() throws
Exception {
+ repConfig = new ECReplicationConfig(3, 2, ECReplicationConfig.EcCodec.RS,
ONEMB);
+ Map<DatanodeDetails, Integer> datanodes = new LinkedHashMap<>();
+ for (int i = 1; i <= repConfig.getRequiredNodes(); i++) {
+ datanodes.put(MockDatanodeDetails.randomDatanodeDetails(), i);
+ }
+
+ BlockLocationInfo keyInfo = ECStreamTestUtil.createKeyInfo(repConfig, 8 *
ONEMB, datanodes);
+ OzoneClientConfig clientConfig = conf.getObject(OzoneClientConfig.class);
+ clientConfig.setChecksumVerify(true);
+
+ try (ECBlockInputStream ecb = new ECBlockInputStream(repConfig,
+ keyInfo, null, null, streamFactory, clientConfig)) {
+
+ // Read a full stripe first to initialize and create streams in the
factory
+ ByteBuffer buf = ByteBuffer.allocate(3 * ONEMB);
+ int read = ecb.read(buf);
+ assertEquals(3 * ONEMB, read);
+
+ // Simulate the Bug: Force the underlying stream to return 0 bytes
(Short Read).
+ // Note: If the test stub `TestBlockInputStream` does not currently have
+ // a method to simulate a 0-byte read, you should add a simple boolean
flag
+ // like `simulateZeroByteRead` to that stub class, making its read()
return 0.
+ streamFactory.getBlockStreams().get(0).setSimulateZeroByteRead(true);
+
+ buf.clear();
+
+ // Assert that instead of spinning infinitely, the short read (0 bytes)
+ // immediately triggers the strict validation and throws
BadDataLocationException.
+ // This exception is essential for the Proxy to initiate the Failover to
Reconstruction.
+ BadDataLocationException e =
assertThrows(BadDataLocationException.class, () -> ecb.read(buf));
+ List<DatanodeDetails> failed = e.getFailedLocations();
+
+ // Expect exactly 1 DN reported as failure due to the inconsistent read
+ assertEquals(1, failed.size());
+ // The failure should map to index = 1 (stream 0)
+ assertEquals(1, datanodes.get(failed.get(0)));
+ }
+ }
+
private void validateBufferContents(ByteBuffer buf, int from, int to,
byte val) {
for (int i = from; i < to; i++) {
@@ -593,6 +637,7 @@ private static class TestBlockInputStream extends
BlockExtendedInputStream {
private BlockID blockID;
private long length;
private boolean throwException = false;
+ private boolean simulateZeroByteRead = false;
private static final byte EOF = -1;
@SuppressWarnings("checkstyle:parameternumber")
@@ -610,6 +655,10 @@ public void setThrowException(boolean shouldThrow) {
this.throwException = shouldThrow;
}
+ public void setSimulateZeroByteRead(boolean simulateZeroByteRead) {
+ this.simulateZeroByteRead = simulateZeroByteRead;
+ }
+
@Override
public BlockID getBlockID() {
return blockID;
@@ -636,6 +685,10 @@ public int read(ByteBuffer buf) throws IOException {
throw new IOException("Simulated exception");
}
+ if (simulateZeroByteRead) {
+ return 0;
+ }
+
int toRead = Math.min(buf.remaining(), (int)getRemaining());
for (int i = 0; i < toRead; i++) {
buf.put(dataVal);
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]