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 86e97cd3750 HDDS-16396. Support concurrent positioned reads for EC and
encrypted keys (#11348)
86e97cd3750 is described below
commit 86e97cd3750eb451e4cd26e20e62af474f537ede
Author: Peter Lee <[email protected]>
AuthorDate: Mon Oct 12 18:42:15 2026 +0800
HDDS-16396. Support concurrent positioned reads for EC and encrypted keys
(#11348)
---
.../hadoop/hdds/scm/storage/BlockInputStream.java | 2 +-
.../hadoop/hdds/scm/storage/ChunkInputStream.java | 6 +-
.../hdds/scm/storage/ExtendedInputStream.java | 55 +++-
.../hdds/scm/storage/MultipartInputStream.java | 60 +---
.../hadoop/hdds/scm/storage/PartInputStream.java | 17 +-
.../hdds/scm/storage/StreamBlockInputStream.java | 4 +-
.../hadoop/ozone/client/io/ECBlockInputStream.java | 12 +
.../ozone/client/io/ECBlockInputStreamProxy.java | 39 ++-
.../client/io/ECBlockReconstructedInputStream.java | 12 +
.../hdds/scm/storage/PositionedReadTestHelper.java | 3 +-
.../hdds/scm/storage/TestBlockInputStream.java | 11 +
.../hdds/scm/storage/TestChunkInputStream.java | 3 +
.../hdds/scm/storage/TestMultipartInputStream.java | 54 ++--
.../scm/storage/TestStreamBlockInputStream.java | 117 +++++++-
.../hadoop/ozone/client/io/ECStreamTestUtil.java | 15 +-
.../ozone/client/io/TestECBlockInputStream.java | 12 +
.../client/io/TestECBlockInputStreamProxy.java | 137 +++++++++
.../io/TestECBlockReconstructedInputStream.java | 39 +++
.../ozone/client/io/OzoneCryptoInputStream.java | 91 ++----
.../hadoop/ozone/client/io/OzoneInputStream.java | 39 ++-
.../client/io/TestOzoneCryptoInputStream.java | 88 +++++-
hadoop-ozone/integration-test/pom.xml | 6 +
.../hadoop/fs/ozone/TestOzoneFSInputStream.java | 38 +++
.../client/rpc/TestOzoneAtRestEncryption.java | 26 ++
.../ozone/client/rpc/read/TestStreamRead.java | 9 +-
.../hadoop/fs/ozone/CapableOzoneFSInputStream.java | 4 +-
.../apache/hadoop/fs/ozone/OzoneFSInputStream.java | 147 ++--------
.../hadoop/fs/ozone/TestOzoneFSInputStream.java | 325 ++++++++-------------
28 files changed, 843 insertions(+), 528 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 c3b1f3ed73f..d29fe986c65 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
@@ -476,7 +476,7 @@ protected synchronized int
readWithStrategy(ByteReaderStrategy strategy)
@Override
public int readPositioned(long blockRelativePosition, ByteBuffer dst) throws
IOException {
if (!dst.hasRemaining()) {
- return EOF;
+ return 0;
}
if (blockRelativePosition < 0) {
return EOF;
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 67f769f56e4..aa811a5734a 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
@@ -446,13 +446,13 @@ protected void readChunkDataIntoBuffers(ChunkInfo
readChunkInfo)
* @return number of bytes copied into {@code dst}, or {@link #EOF} at EOF
*/
int readPositioned(long chunkRelativePosition, ByteBuffer dst) throws
IOException {
+ if (!dst.hasRemaining()) {
+ return 0;
+ }
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();
diff --git
a/hadoop-hdds/client/src/main/java/org/apache/hadoop/hdds/scm/storage/ExtendedInputStream.java
b/hadoop-hdds/client/src/main/java/org/apache/hadoop/hdds/scm/storage/ExtendedInputStream.java
index 75e483ad55e..2a61c40608d 100644
---
a/hadoop-hdds/client/src/main/java/org/apache/hadoop/hdds/scm/storage/ExtendedInputStream.java
+++
b/hadoop-hdds/client/src/main/java/org/apache/hadoop/hdds/scm/storage/ExtendedInputStream.java
@@ -17,12 +17,15 @@
package org.apache.hadoop.hdds.scm.storage;
+import java.io.EOFException;
import java.io.IOException;
-import java.io.InputStream;
import java.nio.ByteBuffer;
+import java.nio.ReadOnlyBufferException;
import org.apache.commons.lang3.NotImplementedException;
+import org.apache.hadoop.fs.ByteBufferPositionedReadable;
import org.apache.hadoop.fs.ByteBufferReadable;
import org.apache.hadoop.fs.CanUnbuffer;
+import org.apache.hadoop.fs.FSInputStream;
import org.apache.hadoop.fs.Seekable;
import org.apache.hadoop.fs.StreamCapabilities;
import org.apache.hadoop.util.StringUtils;
@@ -31,20 +34,51 @@
* Abstact class which extends InputStream and some common interfaces used by
* various Ozone InputStream classes.
*/
-public abstract class ExtendedInputStream extends InputStream
- implements Seekable, CanUnbuffer, ByteBufferReadable, StreamCapabilities {
+public abstract class ExtendedInputStream extends FSInputStream
+ implements Seekable, CanUnbuffer, ByteBufferReadable,
ByteBufferPositionedReadable, StreamCapabilities {
protected static final int EOF = -1;
+ @Override
+ public int read(long position, ByteBuffer buffer) throws IOException {
+ if (position < 0) {
+ throw new EOFException("position is negative: " + position);
+ }
+ if (buffer.isReadOnly()) {
+ throw new ReadOnlyBufferException();
+ }
+ return buffer.hasRemaining() ? readPositioned(position, buffer) : 0;
+ }
+
/**
- * Positioned read.
- *
- * @param position the starting position of the read.
- * @param buffer the buffer for storing the data.
- * @return true iff positioned read is supported in this implementation.
+ * Read without changing the sequential cursor. Implementations must allow
concurrent calls.
*/
- public boolean readFully(long position, ByteBuffer buffer) throws
IOException {
- return false;
+ protected abstract int readPositioned(long position, ByteBuffer buffer)
throws IOException;
+
+ @Override
+ public void readFully(long position, ByteBuffer buffer) throws IOException {
+ do {
+ int n = read(position, buffer);
+ if (n < 0) {
+ throw new EOFException("End of stream at position " + position);
+ }
+ if (n == 0 && buffer.hasRemaining()) {
+ throw new IOException("No progress reading at position " + position);
+ }
+ position += n;
+ } while (buffer.hasRemaining());
+ }
+
+ @Override
+ public int read(long position, byte[] buffer, int offset, int length) throws
IOException {
+ validatePositionedReadArgs(position, buffer, offset, length);
+ return read(position, ByteBuffer.wrap(buffer, offset, length));
+ }
+
+ @Override
+ public void readFully(long position, byte[] buffer, int offset, int length)
throws IOException {
+ validatePositionedReadArgs(position, buffer, offset, length);
+ readFully(position, ByteBuffer.wrap(buffer, offset, length));
}
@Override
@@ -100,6 +134,7 @@ public synchronized boolean seekToNewSource(long l) throws
IOException {
@Override
public boolean hasCapability(String capability) {
switch (StringUtils.toLowerCase(capability)) {
+ case StreamCapabilities.PREADBYTEBUFFER:
case StreamCapabilities.READBYTEBUFFER:
case StreamCapabilities.UNBUFFER:
return true;
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 00a119690b0..d62c1bb42f7 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
@@ -27,7 +27,6 @@
import java.util.List;
import java.util.Objects;
import org.apache.hadoop.fs.FSExceptionMessages;
-import org.apache.ratis.util.Preconditions;
/**
* A stream for accessing multipart streams.
@@ -39,8 +38,6 @@ 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.
@@ -50,7 +47,7 @@ public class MultipartInputStream extends ExtendedInputStream
{
// part[0]), partOffsets[1] = 200 and so on.
private final long[] partOffsets;
- private boolean closed;
+ private volatile boolean closed;
// Index of the partStream corresponding to the current position of the
// MultipartCryptoKeyInputStream.
private int partIndex;
@@ -67,30 +64,18 @@ public MultipartInputStream(String keyName,
this.key = keyName;
this.partStreams = Collections.unmodifiableList(inputStreams);
- this.isStreamBlockInputStream = !inputStreams.isEmpty() &&
inputStreams.get(0) instanceof StreamBlockInputStream;
// 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;
}
- public boolean isStreamBlockInputStream() {
- return isStreamBlockInputStream;
- }
-
@Override
protected synchronized int readWithStrategy(ByteReaderStrategy strategy)
throws IOException {
@@ -195,45 +180,28 @@ public synchronized void seek(long pos) throws
IOException {
}
@Override
- public boolean readFully(long position, ByteBuffer buffer) throws
IOException {
- if (!statelessPositionedReadSupported) {
- return false;
- }
- return readFullyStateless(position, buffer);
- }
-
- /**
- * Shared stateless positioned-read loop across parts using {@link
PartInputStream#readPositioned}.
- */
- private boolean readFullyStateless(long position, ByteBuffer buffer) throws
IOException {
+ protected int readPositioned(long position, ByteBuffer buffer) throws
IOException {
+ checkOpen();
if (!buffer.hasRemaining()) {
- return true;
+ return 0;
}
-
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);
+ while (buffer.hasRemaining() && pos < length) {
+ int index = binarySearchOffsetIndex(partOffsets, pos);
+ PartInputStream part = partStreams.get(index);
+ long partPosition = pos - partOffsets[index];
+ final int n = part.read(partPosition, buffer);
+ if (n < 0) {
+ break;
}
- int idx = binarySearchOffsetIndex(partOffsets, pos);
- long partPos = pos - partOffsets[idx];
- int n = partStreams.get(idx).readPositioned(partPos, buffer);
- if (n <= 0) {
- if (bytesRead > 0) {
- return true;
- }
- throw new EOFException("EOF encountered at pos: " + pos +
- " for key: " + key);
+ if (n == 0) {
+ throw new IOException("No progress reading key " + key + " at position
" + pos);
}
bytesRead += n;
pos += n;
}
- return true;
+ return bytesRead == 0 ? EOF : bytesRead;
}
static int binarySearchOffsetIndex(long[] offsets, long pos) {
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 3d7ee44c58e..18f89053451 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,15 +18,16 @@
package org.apache.hadoop.hdds.scm.storage;
import java.io.IOException;
-import java.nio.ByteBuffer;
+import org.apache.hadoop.fs.ByteBufferPositionedReadable;
import org.apache.hadoop.fs.CanUnbuffer;
import org.apache.hadoop.fs.Seekable;
/**
* A stream that can be a part of a {@link MultipartInputStream}.
+ * Positioned reads must allow concurrent calls without changing the
sequential cursor.
*/
public interface PartInputStream
- extends CanUnbuffer, Seekable {
+ extends CanUnbuffer, Seekable, ByteBufferPositionedReadable {
long getLength();
default long getRemaining() throws IOException {
@@ -34,16 +35,4 @@ 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 8d936215b66..c162553ee81 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
@@ -185,12 +185,12 @@ synchronized int readFully(ByteBuffer targetBuf, boolean
preRead) throws IOExcep
*
* @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.
+ * @return the number of bytes copied into {@code dst}, or -1 at EOF.
*/
@Override
public int readPositioned(long blockOffset, ByteBuffer dst) throws
IOException {
if (!dst.hasRemaining()) {
- return EOF;
+ return 0;
}
if (blockOffset < 0 || blockOffset >= blockLength) {
return EOF;
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 eb876642bef..b655e5d3a39 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
@@ -31,6 +31,7 @@
import java.util.TreeMap;
import java.util.function.Function;
import org.apache.hadoop.fs.FSExceptionMessages;
+import org.apache.hadoop.fs.StreamCapabilities;
import org.apache.hadoop.hdds.client.BlockID;
import org.apache.hadoop.hdds.client.ContainerBlockID;
import org.apache.hadoop.hdds.client.ECReplicationConfig;
@@ -300,6 +301,17 @@ protected long remaining() {
return blockLength() - position;
}
+ @Override
+ protected int readPositioned(long offset, ByteBuffer buffer) {
+ throw new UnsupportedOperationException("Use ECBlockInputStreamProxy for
positioned reads");
+ }
+
+ @Override
+ public boolean hasCapability(String capability) {
+ return !StreamCapabilities.PREADBYTEBUFFER.equalsIgnoreCase(capability)
+ && super.hasCapability(capability);
+ }
+
@Override
public synchronized int read(byte[] b, int off, int len) throws IOException {
return read(ByteBuffer.wrap(b, off, len));
diff --git
a/hadoop-hdds/client/src/main/java/org/apache/hadoop/ozone/client/io/ECBlockInputStreamProxy.java
b/hadoop-hdds/client/src/main/java/org/apache/hadoop/ozone/client/io/ECBlockInputStreamProxy.java
index 3411f028841..c9d71f128b5 100644
---
a/hadoop-hdds/client/src/main/java/org/apache/hadoop/ozone/client/io/ECBlockInputStreamProxy.java
+++
b/hadoop-hdds/client/src/main/java/org/apache/hadoop/ozone/client/io/ECBlockInputStreamProxy.java
@@ -19,10 +19,10 @@
import java.io.IOException;
import java.nio.ByteBuffer;
-import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
+import java.util.concurrent.CopyOnWriteArrayList;
import java.util.function.Function;
import org.apache.hadoop.hdds.client.BlockID;
import org.apache.hadoop.hdds.client.ECReplicationConfig;
@@ -56,8 +56,8 @@ public class ECBlockInputStreamProxy extends
BlockExtendedInputStream {
private BlockExtendedInputStream blockReader;
private boolean reconstructionReader = false;
- private List<DatanodeDetails> failedLocations = new ArrayList<>();
- private boolean closed = false;
+ private final CopyOnWriteArrayList<DatanodeDetails> failedLocations;
+ private volatile boolean closed = false;
private OzoneClientConfig config;
/**
@@ -102,12 +102,21 @@ public ECBlockInputStreamProxy(ECReplicationConfig
repConfig,
BlockLocationInfo> refreshFunction,
ECBlockInputStreamFactory streamFactory,
OzoneClientConfig config) {
+ this(repConfig, blockInfo, xceiverClientFactory, refreshFunction,
streamFactory, config,
+ new CopyOnWriteArrayList<>());
+ }
+
+ private ECBlockInputStreamProxy(ECReplicationConfig repConfig,
BlockLocationInfo blockInfo,
+ XceiverClientFactory xceiverClientFactory, Function<BlockID,
BlockLocationInfo> refreshFunction,
+ ECBlockInputStreamFactory streamFactory, OzoneClientConfig config,
+ CopyOnWriteArrayList<DatanodeDetails> failedLocations) {
this.repConfig = repConfig;
this.blockInfo = blockInfo;
this.ecBlockInputStreamFactory = streamFactory;
this.xceiverClientFactory = xceiverClientFactory;
this.refreshFunction = refreshFunction;
this.config = config;
+ this.failedLocations = failedLocations;
setReaderType();
createBlockReader();
@@ -116,9 +125,10 @@ public ECBlockInputStreamProxy(ECReplicationConfig
repConfig,
private synchronized void setReaderType() {
int expected = expectedDataLocations(repConfig, getLength());
int available = availableDataLocations(blockInfo.getPipeline(), expected);
- reconstructionReader = available < expected;
+ reconstructionReader = available < expected || !failedLocations.isEmpty();
if (reconstructionReader) {
- LOG.info("Data locations available: {} < expected: {}, using
reconstruction read", available, expected);
+ LOG.info("Data locations available: {}, expected: {}, failed: {}, using
reconstruction read",
+ available, expected, failedLocations.size());
}
}
@@ -197,11 +207,28 @@ public synchronized int read(ByteBuffer buf) throws
IOException {
return totalRead;
}
+ @Override
+ protected int readPositioned(long position, ByteBuffer buffer) throws
IOException {
+ ensureNotClosed();
+ if (!buffer.hasRemaining()) {
+ return 0;
+ }
+ if (position >= blockInfo.getLength()) {
+ return EOF;
+ }
+ // Each request owns its cursor and reconstruction buffers; failed
locations are shared.
+ try (ECBlockInputStreamProxy reader = new
ECBlockInputStreamProxy(repConfig, blockInfo,
+ xceiverClientFactory, refreshFunction, ecBlockInputStreamFactory,
config, failedLocations)) {
+ reader.seek(position);
+ return reader.read(buffer);
+ }
+ }
+
private synchronized void failoverToReconstructionRead(
List<DatanodeDetails> badLocations, long lastPosition)
throws IOException {
if (badLocations != null) {
- failedLocations.addAll(badLocations);
+ failedLocations.addAllAbsent(badLocations);
}
blockReader.close();
reconstructionReader = true;
diff --git
a/hadoop-hdds/client/src/main/java/org/apache/hadoop/ozone/client/io/ECBlockReconstructedInputStream.java
b/hadoop-hdds/client/src/main/java/org/apache/hadoop/ozone/client/io/ECBlockReconstructedInputStream.java
index 0d0c2ad438c..256d41eeac3 100644
---
a/hadoop-hdds/client/src/main/java/org/apache/hadoop/ozone/client/io/ECBlockReconstructedInputStream.java
+++
b/hadoop-hdds/client/src/main/java/org/apache/hadoop/ozone/client/io/ECBlockReconstructedInputStream.java
@@ -20,6 +20,7 @@
import java.io.EOFException;
import java.io.IOException;
import java.nio.ByteBuffer;
+import org.apache.hadoop.fs.StreamCapabilities;
import org.apache.hadoop.hdds.client.BlockID;
import org.apache.hadoop.hdds.client.ECReplicationConfig;
import org.apache.hadoop.hdds.scm.storage.BlockExtendedInputStream;
@@ -49,6 +50,17 @@ public ECBlockReconstructedInputStream(ECReplicationConfig
repConfig,
this.stripeReader = stripeReader;
}
+ @Override
+ protected int readPositioned(long offset, ByteBuffer buffer) {
+ throw new UnsupportedOperationException("Use ECBlockInputStreamProxy for
positioned reads");
+ }
+
+ @Override
+ public boolean hasCapability(String capability) {
+ return !StreamCapabilities.PREADBYTEBUFFER.equalsIgnoreCase(capability)
+ && super.hasCapability(capability);
+ }
+
@Override
public synchronized BlockID getBlockID() {
return stripeReader.getBlockID();
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
index fe5514698ff..42fef84799e 100644
---
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
@@ -60,7 +60,8 @@ public static void runConcurrentPositionedReads(byte[] source,
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);
+ int offset = (int) (((long) threadId * source.length /
THREAD_COUNT + i * 17L)
+ % (source.length - BUFFER_SIZE));
ByteBuffer buf = ByteBuffer.allocate(BUFFER_SIZE);
action.readAtOffset(offset, buf);
buf.flip();
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 baf4d7fef09..22416df3af4 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
@@ -538,6 +538,17 @@ private static Stream<Arguments>
exceptionsTriggersRefresh() {
);
}
+ @Test
+ public void testPositionedReadAtEof() throws Exception {
+ ByteBuffer empty = ByteBuffer.allocate(0);
+ for (long position : new long[] {0, blockSize, blockSize + 1}) {
+ assertEquals(0, blockStream.readPositioned(position, empty));
+ assertEquals(0, blockStream.read(position, empty));
+ }
+ assertEquals(-1, blockStream.readPositioned(blockSize,
ByteBuffer.allocate(1)));
+ assertEquals(-1, blockStream.readPositioned(blockSize + 1,
ByteBuffer.allocate(1)));
+ }
+
@Test
public void testConcurrentPositionedRead() throws Exception {
try (BlockInputStream largeBlockStream = createLargeBlockStream()) {
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 96d911319f0..457d7a0a0ec 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
@@ -189,6 +189,9 @@ 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));
+ for (long position : new long[] {0, CHUNK_SIZE, CHUNK_SIZE + 1}) {
+ assertEquals(0, chunkStream.readPositioned(position,
ByteBuffer.allocate(0)));
+ }
}
@Test
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 7ac64376390..8eb81ed7069 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
@@ -19,8 +19,9 @@
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.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.Mockito.doAnswer;
@@ -30,8 +31,10 @@
import static org.mockito.Mockito.when;
import com.google.common.primitives.Bytes;
+import java.io.EOFException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
@@ -47,6 +50,8 @@
import org.apache.hadoop.hdds.scm.pipeline.Pipeline;
import org.apache.hadoop.ozone.common.Checksum;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
import org.mockito.invocation.InvocationOnMock;
/**
@@ -75,8 +80,15 @@ public void testPositionedReadPartialAtEof() throws
Exception {
int position = 12;
int expectedBytes = fileLen - position;
ByteBuffer buffer = ByteBuffer.allocate(expectedBytes * 2);
- assertTrue(multipartStream.readFully(position, buffer));
+ assertEquals(expectedBytes, multipartStream.read(position, buffer));
+ buffer.clear();
+ assertThrows(EOFException.class, () ->
multipartStream.readFully(position, buffer));
assertEquals(expectedBytes, buffer.position());
+ ByteBuffer empty = ByteBuffer.allocate(0);
+ for (long offset : new long[] {0, fileLen, fileLen + 1}) {
+ assertEquals(0, multipartStream.readPositioned(offset, empty));
+ assertEquals(0, multipartStream.read(offset, empty));
+ }
}
}
@@ -88,11 +100,12 @@ public void testStreamBlockPositionedReadPartialAtEof()
throws Exception {
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, multipartStream.read(position, buffer));
+ buffer.clear();
+ assertThrows(EOFException.class, () ->
multipartStream.readFully(position, buffer));
assertEquals(expectedBytes, buffer.position());
verify(part, never()).seek(anyLong());
}
@@ -111,20 +124,16 @@ public void testStreamBlockConcurrentPositionedRead()
throws Exception {
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);
- }
- });
+ multipartStream::readFully);
verify(part0, never()).seek(anyLong());
verify(part1, never()).seek(anyLong());
}
}
- @Test
- public void testConcurrentPositionedRead() throws Exception {
+ @ParameterizedTest
+ @CsvSource({"false, false", "true, false", "false, true"})
+ public void testConcurrentPositionedRead(boolean streamPart0, boolean
streamPart1) throws Exception {
byte[] part0Data = generateRandomData(PART_SIZE);
byte[] part1Data = generateRandomData(PART_SIZE);
byte[] keyData = Bytes.concat(part0Data, part1Data);
@@ -136,22 +145,23 @@ public void testConcurrentPositionedRead() throws
Exception {
Function<BlockID, BlockLocationInfo> refreshFunction =
mock(Function.class);
Checksum checksum = new Checksum(ChecksumType.NONE, 1024);
- BlockInputStream part0 = createBlockStream(new BlockID(new
ContainerBlockID(1, 1)),
+ PartInputStream part0 = streamPart0 ? stubStreamBlockPart(part0Data)
+ : createBlockStream(new BlockID(new ContainerBlockID(1, 1)),
part0Data, pipeline, refreshFunction, clientConfig, checksum);
- BlockInputStream part1 = createBlockStream(new BlockID(new
ContainerBlockID(1, 2)),
+ PartInputStream part1 = streamPart1 ? stubStreamBlockPart(part1Data)
+ : createBlockStream(new BlockID(new ContainerBlockID(1, 2)),
part1Data, pipeline, refreshFunction, clientConfig, checksum);
- List<BlockInputStream> parts = new ArrayList<>();
+ List<PartInputStream> 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);
- }
- });
+ multipartStream::readFully);
+ ByteBuffer buffer = ByteBuffer.allocate(4);
+ multipartStream.readFully(PART_SIZE - 2, buffer);
+ assertArrayEquals(Arrays.copyOfRange(keyData, PART_SIZE - 2, PART_SIZE +
2), buffer.array());
+ assertEquals(0, multipartStream.getPos());
}
}
@@ -159,7 +169,7 @@ private static StreamBlockInputStream
stubStreamBlockPart(byte[] partData) throw
StreamBlockInputStream part = mock(StreamBlockInputStream.class);
when(part.getLength()).thenReturn((long) partData.length);
doAnswer(invocation -> readPositionedFrom(partData, invocation)).when(part)
- .readPositioned(anyLong(), any(ByteBuffer.class));
+ .read(anyLong(), any(ByteBuffer.class));
return part;
}
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 6132f1e9dc5..ceb4497fd33 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
@@ -24,6 +24,7 @@
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.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.eq;
@@ -36,6 +37,7 @@
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
+import java.io.EOFException;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.time.Duration;
@@ -45,6 +47,11 @@
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
@@ -65,6 +72,7 @@
import org.apache.hadoop.hdds.scm.StreamingReaderSpi;
import org.apache.hadoop.hdds.scm.XceiverClientFactory;
import org.apache.hadoop.hdds.scm.XceiverClientGrpc;
+import org.apache.hadoop.hdds.scm.XceiverClientSpi;
import
org.apache.hadoop.hdds.scm.container.common.helpers.StorageContainerException;
import org.apache.hadoop.hdds.scm.pipeline.Pipeline;
import org.apache.hadoop.hdds.security.token.OzoneBlockTokenIdentifier;
@@ -87,6 +95,95 @@ public class TestStreamBlockInputStream {
private static final Duration STREAM_READ_TIMEOUT = Duration.ofSeconds(5);
private static final Function<BlockID, BlockLocationInfo> NO_REFRESH = b ->
null;
+ @Test
+ public void testPositionedReadsOverlapWithoutMovingCursor() throws Exception
{
+ XceiverClientGrpc client = mock(XceiverClientGrpc.class);
+ XceiverClientFactory factory = mock(XceiverClientFactory.class);
+ when(factory.acquireClientForReadData(any())).thenReturn(client);
+ Map<StreamingReadResponse, StreamingReaderSpi> readers = new
ConcurrentHashMap<>();
+ doAnswer(inv -> {
+ StreamingReaderSpi reader = inv.getArgument(1);
+ StreamingReadResponse response = new StreamingReadResponse(
+ MockDatanodeDetails.randomDatanodeDetails(),
mock(ClientCallStreamObserver.class));
+ reader.setStreamingReadResponse(response);
+ readers.put(response, reader);
+ return null;
+ }).when(client).initStreamRead(any(), any(), any());
+ CountDownLatch started = new CountDownLatch(2);
+ doAnswer(inv -> {
+ ContainerCommandRequestProto request = inv.getArgument(0);
+ started.countDown();
+ assertTrue(started.await(5, TimeUnit.SECONDS), "positioned reads must
overlap");
+ long offset = request.getReadBlock().getOffset();
+ assertEquals(2, request.getReadBlock().getLength(), "positioned reads
must not request read-ahead");
+ StreamingReaderSpi reader = readers.get(inv.getArgument(1));
+ reader.onNext(buildResponseProto(new byte[] {(byte) offset}, offset));
+ reader.onNext(buildResponseProto(new byte[] {(byte) (offset + 1)},
offset + 1));
+ reader.onCompleted();
+ return null;
+ }).when(client).streamRead(any(), any());
+ ExecutorService executor = Executors.newFixedThreadPool(2);
+ try (StreamBlockInputStream stream = new StreamBlockInputStream(new
BlockID(1, 22), 100,
+ mockStandalonePipeline(), null, factory, NO_REFRESH,
newStreamReadConfig())) {
+ stream.seek(50);
+ Future<?> first = executor.submit(() -> {
+ ByteBuffer buffer = ByteBuffer.allocate(2);
+ stream.readFully(10, buffer);
+ assertArrayEquals(new byte[] {10, 11}, buffer.array());
+ return null;
+ });
+ Future<?> second = executor.submit(() -> {
+ byte[] buffer = new byte[2];
+ stream.readFully(20, buffer);
+ assertArrayEquals(new byte[] {20, 21}, buffer);
+ return null;
+ });
+ first.get(10, TimeUnit.SECONDS);
+ second.get(10, TimeUnit.SECONDS);
+ assertEquals(50, stream.getPos());
+ } finally {
+ executor.shutdownNow();
+ }
+ verify(factory, times(2)).releaseClientForReadData(client, false);
+ verify(client, times(2)).completeStreamRead();
+ for (StreamingReadResponse response : readers.keySet()) {
+ verify(response.getRequestObserver()).onCompleted();
+ }
+ }
+
+ @Test
+ public void testIncompletePositionedReadReleasesResources() throws Exception
{
+ ClientCallStreamObserver<ContainerCommandRequestProto> observer =
mock(ClientCallStreamObserver.class);
+ XceiverClientGrpc client = mockCapturingStreamingReadClient(observer,
StreamingReaderSpi::onCompleted);
+ XceiverClientFactory factory = mock(XceiverClientFactory.class);
+ when(factory.acquireClientForReadData(any())).thenReturn(client);
+ try (StreamBlockInputStream stream = new StreamBlockInputStream(new
BlockID(1, 23), 100,
+ mockStandalonePipeline(), null, factory, NO_REFRESH,
newStreamReadConfig())) {
+ stream.seek(50);
+ ByteBuffer buffer = ByteBuffer.allocate(2);
+ assertThrows(EOFException.class, () -> stream.readFully(10, buffer));
+ assertEquals(0, buffer.position());
+ assertEquals(50, stream.getPos());
+ }
+ verify(factory).releaseClientForReadData(client, false);
+ verify(client).completeStreamRead();
+ verify(observer).onCompleted();
+ }
+
+ @Test
+ public void testFailedPositionedReadInitializationReleasesClient() throws
Exception {
+ XceiverClientGrpc client = mock(XceiverClientGrpc.class);
+ doThrow(new IOException("initialization
failed")).when(client).initStreamRead(any(), any(), any());
+ XceiverClientFactory factory = mock(XceiverClientFactory.class);
+ when(factory.acquireClientForReadData(any())).thenReturn(client);
+ try (StreamBlockInputStream stream = new StreamBlockInputStream(new
BlockID(1, 24), 100,
+ mockStandalonePipeline(), null, factory, NO_REFRESH,
newStreamReadConfig())) {
+ assertThrows(IOException.class, () -> stream.readFully(10,
ByteBuffer.allocate(2)));
+ }
+ verify(factory).releaseClientForReadData(client, false);
+ verify(client, never()).completeStreamRead();
+ }
+
@Test
public void testCustomStreamReadConfigIsApplied() throws Exception {
// Arrange: create a config with non-default values
@@ -152,8 +249,12 @@ public void testPositionedReadAtEof() throws Exception {
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)));
+ ByteBuffer empty = ByteBuffer.allocate(1);
+ empty.position(1);
+ for (long position : new long[] {0, data.length, data.length + 1}) {
+ assertEquals(0, sbis.readPositioned(position, empty));
+ assertEquals(0, sbis.read(position, empty));
+ }
}
}
@@ -183,6 +284,18 @@ public void
testPositionedReadDoesNotReleasePermitWhenInitStreamReadFails() thro
verify(xceiverClient, never()).completeStreamRead();
}
+ @Test
+ public void testPositionedReadReleasesUnexpectedClient() throws Exception {
+ XceiverClientSpi client = mock(XceiverClientSpi.class);
+ XceiverClientFactory factory = mock(XceiverClientFactory.class);
+
when(factory.acquireClientForReadData(any(Pipeline.class))).thenReturn(client);
+ try (StreamBlockInputStream stream = new StreamBlockInputStream(
+ new BlockID(1, 25), 64, mockStandalonePipeline(), null, factory,
NO_REFRESH, newStreamReadConfig())) {
+ assertThrows(IOException.class, () -> stream.readPositioned(0,
ByteBuffer.allocate(1)));
+ }
+ verify(factory).releaseClientForReadData(client, false);
+ }
+
@Test
public void testConcurrentPositionedRead() throws Exception {
byte[] data = TestChunkInputStream.generateRandomData(SOURCE_SIZE);
diff --git
a/hadoop-hdds/client/src/test/java/org/apache/hadoop/ozone/client/io/ECStreamTestUtil.java
b/hadoop-hdds/client/src/test/java/org/apache/hadoop/ozone/client/io/ECStreamTestUtil.java
index 639b4e92a14..d84540ec8ac 100644
---
a/hadoop-hdds/client/src/test/java/org/apache/hadoop/ozone/client/io/ECStreamTestUtil.java
+++
b/hadoop-hdds/client/src/test/java/org/apache/hadoop/ozone/client/io/ECStreamTestUtil.java
@@ -266,7 +266,7 @@ public synchronized BlockExtendedInputStream create(
int repInd = currentPipeline.getReplicaIndex(pipeline.getNodes().get(0));
TestBlockInputStream stream = new TestBlockInputStream(
blockInfo.getBlockID(), blockInfo.getLength(),
- blockStreamData.get(repInd - 1), repInd);
+ blockStreamData.get(repInd - 1).duplicate(), repInd);
if (failIndexes.remove(Integer.valueOf(repInd))) {
stream.setShouldError(true);
}
@@ -324,6 +324,19 @@ public int getEcReplicaIndex() {
return ecReplicaIndex;
}
+ @Override
+ protected int readPositioned(long position, ByteBuffer buffer) {
+ if (position >= length) {
+ return EOF;
+ }
+ ByteBuffer source = data.duplicate();
+ source.position((int) position);
+ int n = (int) Math.min(buffer.remaining(), length - position);
+ source.limit(source.position() + n);
+ buffer.put(source);
+ return n;
+ }
+
@Override
public BlockID getBlockID() {
return blockID;
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 97f9230b471..e85ca219b86 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
@@ -659,6 +659,18 @@ public void setSimulateZeroByteRead(boolean
simulateZeroByteRead) {
this.simulateZeroByteRead = simulateZeroByteRead;
}
+ @Override
+ protected int readPositioned(long offset, ByteBuffer buffer) {
+ if (offset >= length) {
+ return EOF;
+ }
+ int n = (int) Math.min(buffer.remaining(), length - offset);
+ for (int i = 0; i < n; i++) {
+ buffer.put(dataVal);
+ }
+ return n;
+ }
+
@Override
public BlockID getBlockID() {
return blockID;
diff --git
a/hadoop-hdds/client/src/test/java/org/apache/hadoop/ozone/client/io/TestECBlockInputStreamProxy.java
b/hadoop-hdds/client/src/test/java/org/apache/hadoop/ozone/client/io/TestECBlockInputStreamProxy.java
index 074e6f1f9a8..42162960c2f 100644
---
a/hadoop-hdds/client/src/test/java/org/apache/hadoop/ozone/client/io/TestECBlockInputStreamProxy.java
+++
b/hadoop-hdds/client/src/test/java/org/apache/hadoop/ozone/client/io/TestECBlockInputStreamProxy.java
@@ -18,16 +18,26 @@
package org.apache.hadoop.ozone.client.io;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import java.io.IOException;
import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.SplittableRandom;
+import java.util.concurrent.Callable;
+import java.util.concurrent.CyclicBarrier;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
import java.util.concurrent.ThreadLocalRandom;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Function;
import org.apache.hadoop.hdds.client.BlockID;
import org.apache.hadoop.hdds.client.ECReplicationConfig;
@@ -40,6 +50,8 @@
import org.apache.hadoop.hdds.scm.storage.BlockLocationInfo;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
/**
* Unit tests for the ECBlockInputStreamProxy class.
@@ -328,6 +340,131 @@ public void testCanSeekToNewPosition() throws IOException
{
}
}
+ @ParameterizedTest
+ @ValueSource(booleans = {false, true})
+ void positionedReadsRememberFailedLocations(boolean sequentialFirst) throws
Exception {
+ byte[] data = generateData(1024).array();
+ BlockLocationInfo info = ECStreamTestUtil.createKeyInfo(repConfig,
data.length,
+ ECStreamTestUtil.createIndexMap(1, 2, 3, 4, 5));
+ DatanodeDetails bad = info.getPipeline().getFirstNode();
+ AtomicInteger failedAttempts = new AtomicInteger();
+ ECBlockInputStreamFactory factory = (missing, failed, replication, block,
xceiver, refresh, config) -> {
+ if (missing) {
+ assertThat(failed).containsExactly(bad);
+ }
+ return new ECStreamTestUtil.TestBlockInputStream(block.getBlockID(),
block.getLength(), ByteBuffer.wrap(data)) {
+ @Override
+ public int read(ByteBuffer destination) throws IOException {
+ if (!missing) {
+ failedAttempts.incrementAndGet();
+ throw new BadDataLocationException(bad, "failed data replica");
+ }
+ return super.read(destination);
+ }
+ };
+ };
+ try (ECBlockInputStreamProxy stream = new
ECBlockInputStreamProxy(repConfig, info, null, null,
+ factory, conf.getObject(OzoneClientConfig.class))) {
+ ByteBuffer first = ByteBuffer.allocate(10);
+ assertEquals(10, sequentialFirst ? stream.read(first) : stream.read(0,
first));
+ for (int offset : new int[] {17, 203}) {
+ ByteBuffer buffer = ByteBuffer.allocate(20);
+ stream.readFully(offset, buffer);
+ assertArrayEquals(Arrays.copyOfRange(data, offset, offset + 20),
buffer.array());
+ }
+ assertEquals(1, failedAttempts.get());
+ assertEquals(sequentialFirst ? 10 : 0, stream.getPos());
+ }
+ }
+
+ @ParameterizedTest
+ @ValueSource(booleans = {false, true})
+ void emptyPositionedReadsDoNotCreateReaders(boolean missing) throws
Exception {
+ int length = 100;
+ generateData(length);
+ BlockLocationInfo info = ECStreamTestUtil.createKeyInfo(repConfig, length,
+ missing ? ECStreamTestUtil.createIndexMap(2, 3, 4, 5) :
ECStreamTestUtil.createIndexMap(1, 2, 3, 4, 5));
+ try (ECBlockInputStreamProxy stream = createBISProxy(repConfig, info)) {
+ BlockExtendedInputStream reader =
streamFactory.getStreams().get(missing);
+ ByteBuffer empty = ByteBuffer.allocate(0);
+ for (long position : new long[] {0, length, length + 1}) {
+ assertEquals(0, stream.readPositioned(position, empty));
+ assertEquals(0, stream.read(position, empty));
+ }
+ assertThat(streamFactory.getStreams().get(missing)).isSameAs(reader);
+ assertEquals(-1, stream.readPositioned(length, ByteBuffer.allocate(1)));
+ assertEquals(-1, stream.readPositioned(length + 1,
ByteBuffer.allocate(1)));
+ }
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {"healthy", "missing", "failover", "failure"})
+ void positionedReadsOverlapAndCloseTheirReaders(String mode) throws
Exception {
+ byte[] data = generateData(1024).array();
+ BlockLocationInfo info = ECStreamTestUtil.createKeyInfo(repConfig,
data.length,
+ "missing".equals(mode) ? ECStreamTestUtil.createIndexMap(2, 3, 4, 5)
+ : ECStreamTestUtil.createIndexMap(1, 2, 3, 4, 5));
+ CyclicBarrier reads = new CyclicBarrier(2);
+ AtomicInteger opened = new AtomicInteger();
+ AtomicInteger closed = new AtomicInteger();
+ ECBlockInputStreamFactory factory = (missing, failed, replication, block,
xceiver, refresh, config) -> {
+ opened.incrementAndGet();
+ return new ECStreamTestUtil.TestBlockInputStream(block.getBlockID(),
block.getLength(), ByteBuffer.wrap(data)) {
+ @Override
+ public int read(ByteBuffer destination) throws IOException {
+ if ("failover".equals(mode) && !missing) {
+ destination.put((byte) 0); // Retry must overwrite bytes from the
failed attempt.
+ throw new
BadDataLocationException(info.getPipeline().getFirstNode(), "failed data
replica");
+ }
+ try {
+ reads.await(10, TimeUnit.SECONDS);
+ } catch (Exception e) {
+ throw new IOException("EC positioned reads did not overlap", e);
+ }
+ if ("failure".equals(mode)) {
+ throw new IOException("injected failure");
+ }
+ return super.read(destination);
+ }
+
+ @Override
+ public void close() {
+ closed.incrementAndGet();
+ }
+ };
+ };
+ ExecutorService pool = Executors.newFixedThreadPool(2);
+ try (ECBlockInputStreamProxy stream = new
ECBlockInputStreamProxy(repConfig, info, null, null,
+ factory, conf.getObject(OzoneClientConfig.class))) {
+ stream.seek(71);
+ List<Future<?>> futures = new ArrayList<>();
+ for (int offset : new int[] {17, 203}) {
+ futures.add(pool.submit((Callable<Void>) () -> {
+ ByteBuffer destination = ByteBuffer.allocateDirect(333);
+ if ("failure".equals(mode)) {
+ assertThrows(IOException.class, () -> stream.readFully(offset,
destination));
+ } else {
+ stream.readFully(offset, destination);
+ destination.flip();
+ byte[] actual = new byte[destination.remaining()];
+ destination.get(actual);
+ assertArrayEquals(Arrays.copyOfRange(data, offset, offset + 333),
actual);
+ }
+ return null;
+ }));
+ }
+ for (Future<?> future : futures) {
+ future.get(20, TimeUnit.SECONDS);
+ }
+ assertEquals(71, stream.getPos());
+ assertEquals(opened.get() - 1, closed.get());
+ assertEquals(-1, stream.read(data.length, ByteBuffer.allocate(1)));
+ } finally {
+ pool.shutdownNow();
+ }
+ assertEquals(opened.get(), closed.get());
+ }
+
private ByteBuffer generateData(int length) {
ByteBuffer data = ByteBuffer.allocate(length);
ECStreamTestUtil.randomFill(data, dataGenerator);
diff --git
a/hadoop-hdds/client/src/test/java/org/apache/hadoop/ozone/client/io/TestECBlockReconstructedInputStream.java
b/hadoop-hdds/client/src/test/java/org/apache/hadoop/ozone/client/io/TestECBlockReconstructedInputStream.java
index 91914494f14..27a01f21eee 100644
---
a/hadoop-hdds/client/src/test/java/org/apache/hadoop/ozone/client/io/TestECBlockReconstructedInputStream.java
+++
b/hadoop-hdds/client/src/test/java/org/apache/hadoop/ozone/client/io/TestECBlockReconstructedInputStream.java
@@ -18,6 +18,7 @@
package org.apache.hadoop.ozone.client.io;
import static
org.apache.hadoop.ozone.client.io.ECStreamTestUtil.generateParity;
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
@@ -28,9 +29,12 @@
import java.util.List;
import java.util.Map;
import java.util.SplittableRandom;
+import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
import java.util.concurrent.ThreadLocalRandom;
+import java.util.concurrent.TimeUnit;
import org.apache.hadoop.hdds.client.BlockID;
import org.apache.hadoop.hdds.client.ECReplicationConfig;
import org.apache.hadoop.hdds.conf.OzoneConfiguration;
@@ -363,6 +367,41 @@ private ByteBuffer[] allocateBuffers(int num, int size) {
return bufs;
}
+ @Test
+ void concurrentPositionedReadsReconstructIndependentStripes() throws
Exception {
+ int chunk = repConfig.getEcChunkSize();
+ int blockLength = chunk * 7 - 101;
+ ByteBuffer[] data = allocateBuffers(repConfig.getData(), chunk * 3);
+ ECStreamTestUtil.randomFill(data, chunk, dataGenerator, blockLength);
+ addDataStreamsToFactory(data, generateParity(data, repConfig));
+ byte[] expected = new byte[blockLength];
+ ECStreamTestUtil.randomFill(ByteBuffer.wrap(expected), new
SplittableRandom(randomSeed));
+ BlockLocationInfo info = ECStreamTestUtil.createKeyInfo(repConfig,
blockLength,
+ ECStreamTestUtil.createIndexMap(2, 3, 4, 5));
+ streamFactory.setCurrentPipeline(info.getPipeline());
+ ECBlockInputStreamFactory factory =
ECBlockInputStreamFactoryImpl.getInstance(streamFactory, bufferPool,
+ () -> ecReconstructExecutor);
+ ExecutorService readers = Executors.newFixedThreadPool(2);
+ try (ECBlockInputStreamProxy stream = new
ECBlockInputStreamProxy(repConfig, info, null, null, factory,
+ conf.getObject(OzoneClientConfig.class))) {
+ List<Future<?>> futures = new ArrayList<>();
+ for (int offset : new int[] {chunk - 37, chunk * 3 - 29}) {
+ futures.add(readers.submit((Callable<Void>) () -> {
+ byte[] actual = new byte[chunk * 3 + 53];
+ stream.readFully(offset, actual);
+ assertArrayEquals(Arrays.copyOfRange(expected, offset, offset +
actual.length), actual);
+ return null;
+ }));
+ }
+ for (Future<?> future : futures) {
+ future.get(20, TimeUnit.SECONDS);
+ }
+ assertEquals(0, stream.getPos());
+ } finally {
+ readers.shutdownNow();
+ }
+ }
+
private void addDataStreamsToFactory(ByteBuffer[] data, ByteBuffer[] parity)
{
List<ByteBuffer> dataStreams = new ArrayList<>(Arrays.asList(data));
dataStreams.addAll(Arrays.asList(parity));
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 705aa84d637..2d2c3943ecb 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
@@ -79,17 +79,6 @@ public int getBufferSize() {
return bufferSize;
}
- /**
- * {@link CryptoInputStream} does not synchronize its own methods, so every
method moving the cursor of
- * this stream is serialized here on the monitor of this stream. Otherwise a
read or a seek could land in
- * the middle of a positioned read and see (or undo) the cursor move that
read does.
- * This covers both the sequential-read API ({@link #read()}, {@link
#read(byte[], int, int)},
- * {@link #read(ByteBuffer)}, {@link #seek(long)}, {@link #getPos()}, {@link
#skip(long)}) and all
- * positioned-read overloads
- * ({@link #read(long, ByteBuffer)}, {@link #readFully(long, ByteBuffer)},
- * {@link #read(long, byte[], int, int)}, {@link #readFully(long, byte[],
int, int)},
- * {@link #readFully(long, byte[])}).
- */
@Override
public synchronized int read(byte[] b, int off, int len) throws IOException {
// CryptoInputStream reads hadoop.security.crypto.buffer.size number of
@@ -188,91 +177,49 @@ 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:
- * the cursor is moved to {@code position}, data is read via {@link
#read(byte[], int, int)} (which
- * handles the Crypto boundary adjustment), and the cursor is restored
before the lock is released.
- *
- * <p>Returns -1 for {@code position >= length}. A read-only {@code dst} is
rejected before any state
- * is mutated. If any exception escapes the read loop the crypto-boundary
adjustment fields are reset so
- * the next sequential read does not fail the precondition in {@code
getNumBytesToRead}.
+ * Reads and decrypts at the given offset without moving the sequential
cursor.
+ * This method needs no synchronization: CryptoInputStream uses separate
buffers and a decryptor for each call.
*/
@Override
- public synchronized int read(long position, ByteBuffer dst) throws
IOException {
- if (!dst.hasRemaining()) {
- return 0;
+ public int read(long position, ByteBuffer dst) throws IOException {
+ if (position < 0) {
+ throw new EOFException("The given position is negative: " + position);
}
if (dst.isReadOnly()) {
throw new ReadOnlyBufferException();
}
- if (position < 0) {
- throw new EOFException("The given position is negative: " + position);
+ if (!dst.hasRemaining()) {
+ return 0;
}
- if (position >= getLength()) {
+ if (position >= length) {
return EOF;
}
-
- final long oldPos = getPos();
- final int readLength = (int) Math.min(dst.remaining(), getLength() -
position);
- final byte[] buffer = new byte[Math.min(readLength, getBufferSize())];
- Throwable failure = null;
+ int limit = dst.limit();
try {
- seek(position);
- int totalReadLen = 0;
- while (totalReadLen < readLength) {
- final int numBytesRead = read(buffer, 0,
- Math.min(buffer.length, readLength - totalReadLen));
- if (numBytesRead <= 0) {
- break;
- }
- dst.put(buffer, 0, numBytesRead);
- totalReadLen += numBytesRead;
- }
- return totalReadLen == 0 ? EOF : totalReadLen;
- } catch (Throwable t) {
- failure = t;
- // Defensively reset crypto-boundary adjustment fields so that the next
- // sequential read does not fail the precondition check in
getNumBytesToRead.
- readPositionAdjustedBy = 0;
- readLengthAdjustedBy = 0;
- throw t;
+ dst.limit(dst.position() + (int) Math.min(dst.remaining(), length -
position));
+ return super.read(position, dst);
} finally {
- try {
- seek(oldPos);
- } catch (IOException e) {
- if (failure == null) {
- throw e;
- }
- failure.addSuppressed(e);
- }
+ dst.limit(limit);
}
}
@Override
- public synchronized void readFully(long position, ByteBuffer dst) throws
IOException {
+ public void readFully(long position, ByteBuffer dst) throws IOException {
int bytesRead;
for (int readCount = 0; dst.hasRemaining(); readCount += bytesRead) {
bytesRead = read(position + readCount, dst);
if (bytesRead < 0) {
throw new EOFException(FSExceptionMessages.EOF_IN_READ_FULLY);
}
+ if (bytesRead == 0) {
+ throw new IOException("No progress reading at position " + (position +
readCount));
+ }
}
}
- /**
- * Byte-array positioned read. Routes through {@link #read(long,
ByteBuffer)} so that the
- * seek-read-restore sequence is covered by the same monitor as all other
cursor-moving operations.
- * Guards against null buffer (IAE) and negative position (EOFException)
before touching ByteBuffer,
- * aligning with the {@link org.apache.hadoop.fs.PositionedReadable}
contract.
- */
@Override
- public synchronized int read(long position, byte[] buffer, int offset, int
len) throws IOException {
+ public int read(long position, byte[] buffer, int offset, int len) throws
IOException {
if (buffer == null) {
throw new IllegalArgumentException("Null buffer");
}
@@ -283,7 +230,7 @@ public synchronized int read(long position, byte[] buffer,
int offset, int len)
}
@Override
- public synchronized void readFully(long position, byte[] buffer, int offset,
int len) throws IOException {
+ public void readFully(long position, byte[] buffer, int offset, int len)
throws IOException {
if (buffer == null) {
throw new IllegalArgumentException("Null buffer");
}
@@ -294,7 +241,7 @@ public synchronized void readFully(long position, byte[]
buffer, int offset, int
}
@Override
- public synchronized void readFully(long position, byte[] buffer) throws
IOException {
+ public void readFully(long position, byte[] buffer) throws IOException {
readFully(position, buffer, 0, buffer.length);
}
diff --git
a/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/io/OzoneInputStream.java
b/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/io/OzoneInputStream.java
index 2366893ef56..2d3e93deb31 100644
---
a/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/io/OzoneInputStream.java
+++
b/hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/io/OzoneInputStream.java
@@ -20,16 +20,19 @@
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
+import org.apache.hadoop.fs.ByteBufferPositionedReadable;
import org.apache.hadoop.fs.ByteBufferReadable;
import org.apache.hadoop.fs.CanUnbuffer;
import org.apache.hadoop.fs.Seekable;
+import org.apache.hadoop.fs.StreamCapabilities;
+import org.apache.hadoop.hdds.scm.storage.ByteReaderStrategy;
+import org.apache.hadoop.hdds.scm.storage.ExtendedInputStream;
/**
* OzoneInputStream is used to read data from Ozone.
* It uses {@link KeyInputStream} for reading the data.
*/
-public class OzoneInputStream extends InputStream implements CanUnbuffer,
- ByteBufferReadable, Seekable {
+public class OzoneInputStream extends ExtendedInputStream {
private final InputStream inputStream;
@@ -66,6 +69,38 @@ public int read(ByteBuffer byteBuffer) throws IOException {
}
}
+ @Override
+ protected int readWithStrategy(ByteReaderStrategy strategy) throws
IOException {
+ return strategy.readFromBlock(inputStream, strategy.getTargetLength());
+ }
+
+ @Override
+ protected int readPositioned(long position, ByteBuffer buffer) throws
IOException {
+ if (inputStream instanceof ByteBufferPositionedReadable) {
+ return ((ByteBufferPositionedReadable) inputStream).read(position,
buffer);
+ }
+ throw new UnsupportedOperationException("Positioned reads are not
supported by "
+ + inputStream.getClass().getName());
+ }
+
+ @Override
+ public boolean hasCapability(String capability) {
+ if (!super.hasCapability(capability)) {
+ return false;
+ }
+ if (inputStream instanceof StreamCapabilities
+ && !((StreamCapabilities) inputStream).hasCapability(capability)) {
+ return false;
+ }
+ if (StreamCapabilities.PREADBYTEBUFFER.equalsIgnoreCase(capability)) {
+ return inputStream instanceof ByteBufferPositionedReadable;
+ }
+ if (StreamCapabilities.READBYTEBUFFER.equalsIgnoreCase(capability)) {
+ return inputStream instanceof ByteBufferReadable;
+ }
+ return StreamCapabilities.UNBUFFER.equalsIgnoreCase(capability) &&
inputStream instanceof CanUnbuffer;
+ }
+
@Override
public synchronized void close() throws IOException {
inputStream.close();
diff --git
a/hadoop-ozone/client/src/test/java/org/apache/hadoop/ozone/client/io/TestOzoneCryptoInputStream.java
b/hadoop-ozone/client/src/test/java/org/apache/hadoop/ozone/client/io/TestOzoneCryptoInputStream.java
index d154479d8b2..41d003b3e31 100644
---
a/hadoop-ozone/client/src/test/java/org/apache/hadoop/ozone/client/io/TestOzoneCryptoInputStream.java
+++
b/hadoop-ozone/client/src/test/java/org/apache/hadoop/ozone/client/io/TestOzoneCryptoInputStream.java
@@ -36,6 +36,7 @@
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.Callable;
+import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
@@ -44,8 +45,10 @@
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.crypto.CryptoCodec;
import org.apache.hadoop.crypto.Encryptor;
+import org.apache.hadoop.fs.ByteBufferPositionedReadable;
import org.apache.hadoop.fs.PositionedReadable;
import org.apache.hadoop.fs.Seekable;
+import org.apache.hadoop.hdds.scm.storage.MultipartInputStream;
import org.apache.hadoop.hdds.scm.storage.PositionedReadTestHelper;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
@@ -104,8 +107,7 @@ void testPositionedReadInMiddle() throws Exception {
@Test
void testPositionedReadCrossesBufferBoundary() throws Exception {
- // The crypto buffer is 8 KB by default; read crossing that boundary
exercises
- // the position/length adjustment logic in
OzoneCryptoInputStream.read(byte[], int, int).
+ // The positioned decryptor must handle an unaligned range across crypto
buffers.
byte[] plaintext = RandomUtils.secure().randomBytes(32 * 1024);
try (OzoneCryptoInputStream s = buildStream(plaintext)) {
// Start 100 bytes before the 8 KB boundary
@@ -319,11 +321,7 @@ void testByteArrayReadFullyDoesNotMoveSequentialCursor()
throws Exception {
@Test
@Timeout(value = 30)
void testConcurrentSkipAndPositionedRead() throws Exception {
- // Verifies that skip() is serialized on the same monitor as positioned
reads.
- // Half the threads continuously seek-then-skip (cursor-moving sequential
ops).
- // The other half do positioned readFully and assert correctness. If
skip() could
- // slip between the positioned read's internal seek and read, the read
would
- // consume data from the wrong offset and assertArrayEquals would fail.
+ // Sequential cursor changes must not affect positioned decryption.
byte[] plaintext = RandomUtils.secure().randomBytes(SOURCE_SIZE);
try (OzoneCryptoInputStream s = buildStream(plaintext)) {
ExecutorService pool = Executors.newFixedThreadPool(THREAD_COUNT);
@@ -392,8 +390,7 @@ void testConcurrentPositionedReadsByteArray() throws
Exception {
@Test
@Timeout(value = 30)
void testConcurrentPositionedReadsMixedApi() throws Exception {
- // Intermix ByteBuffer and byte-array callers on the same stream to verify
- // both APIs share the same monitor and do not interleave.
+ // Intermix ByteBuffer and byte-array callers on the same stream.
byte[] plaintext = RandomUtils.secure().randomBytes(SOURCE_SIZE);
try (OzoneCryptoInputStream s = buildStream(plaintext)) {
PositionedReadTestHelper.runConcurrentPositionedReads(plaintext,
(offset, buf) -> {
@@ -408,19 +405,59 @@ void testConcurrentPositionedReadsMixedApi() throws
Exception {
}
}
+ @Test
+ void positionedReadsOverlapAcrossEncryptedParts() throws Exception {
+ byte[] plaintext = RandomUtils.secure().randomBytes(SOURCE_SIZE);
+ int split = SOURCE_SIZE / 2 + 7;
+ CyclicBarrier reads = new CyclicBarrier(2);
+ try (MultipartInputStream stream = new MultipartInputStream(KEY_NAME,
Arrays.asList(
+ buildStream(Arrays.copyOf(plaintext, split), reads),
+ buildStream(Arrays.copyOfRange(plaintext, split, plaintext.length),
reads)))) {
+ stream.seek(777);
+ ExecutorService pool = Executors.newFixedThreadPool(2);
+ try {
+ List<Future<?>> futures = new ArrayList<>();
+ for (int offset : new int[] {split - 101, split - 203}) {
+ futures.add(pool.submit((Callable<Void>) () -> {
+ ByteBuffer destination = ByteBuffer.allocateDirect(20000);
+ destination.position(3);
+ stream.readFully(offset, destination);
+ destination.flip();
+ destination.position(3);
+ assertArrayEquals(Arrays.copyOfRange(plaintext, offset, offset +
19997), toArray(destination));
+ return null;
+ }));
+ }
+ for (Future<?> future : futures) {
+ future.get(20, TimeUnit.SECONDS);
+ }
+ assertEquals(777, stream.getPos());
+ byte[] sequential = new byte[97];
+ assertEquals(sequential.length, stream.read(sequential));
+ assertArrayEquals(Arrays.copyOfRange(plaintext, 777, 874), sequential);
+ } finally {
+ pool.shutdownNow();
+ }
+ }
+ }
+
/**
* Encrypts {@code plaintext} with the shared key/IV, wraps it in a
* {@link SeekableByteArrayInputStream}, and returns a ready-to-use
* {@link OzoneCryptoInputStream}.
*/
private static OzoneCryptoInputStream buildStream(byte[] plaintext) throws
Exception {
+ return buildStream(plaintext, null);
+ }
+
+ private static OzoneCryptoInputStream buildStream(byte[] plaintext,
CyclicBarrier reads) throws Exception {
Encryptor enc = codec.createEncryptor();
enc.init(key, iv);
ByteBuffer plain = ByteBuffer.wrap(plaintext);
ByteBuffer cipher = ByteBuffer.allocate(plaintext.length);
enc.encrypt(plain, cipher);
- SeekableByteArrayInputStream raw = new
SeekableByteArrayInputStream(cipher.array());
+ SeekableByteArrayInputStream raw = new
SeekableByteArrayInputStream(cipher.array(), reads);
LengthInputStream lin = new LengthInputStream(raw, plaintext.length);
return new OzoneCryptoInputStream(lin, codec, key, iv, KEY_NAME,
PART_INDEX);
}
@@ -436,13 +473,15 @@ private static byte[] toArray(ByteBuffer buf) {
* Used to feed ciphertext into {@link OzoneCryptoInputStream} in tests.
*/
private static final class SeekableByteArrayInputStream extends InputStream
- implements Seekable, PositionedReadable {
+ implements Seekable, PositionedReadable, ByteBufferPositionedReadable {
private final byte[] data;
+ private final CyclicBarrier reads;
private volatile int pos;
- SeekableByteArrayInputStream(byte[] data) {
+ SeekableByteArrayInputStream(byte[] data, CyclicBarrier reads) {
this.data = data;
+ this.reads = reads;
}
@Override
@@ -479,6 +518,31 @@ public boolean seekToNewSource(long targetPos) {
return false;
}
+ @Override
+ public int read(long position, ByteBuffer destination) throws IOException {
+ if (reads != null) {
+ try {
+ reads.await(10, TimeUnit.SECONDS);
+ } catch (Exception e) {
+ throw new IOException("Positioned reads did not overlap", e);
+ }
+ }
+ if (position >= data.length) {
+ return -1;
+ }
+ int n = Math.min(destination.remaining(), data.length - (int) position);
+ destination.put(data, (int) position, n);
+ return n;
+ }
+
+ @Override
+ public void readFully(long position, ByteBuffer destination) throws
IOException {
+ int n = destination.remaining();
+ if (read(position, destination) < n) {
+ throw new EOFException();
+ }
+ }
+
@Override
public int read(long position, byte[] buffer, int offset, int length)
throws IOException {
if (position >= data.length) {
diff --git a/hadoop-ozone/integration-test/pom.xml
b/hadoop-ozone/integration-test/pom.xml
index c4e3d7dad12..e0daabd27f0 100644
--- a/hadoop-ozone/integration-test/pom.xml
+++ b/hadoop-ozone/integration-test/pom.xml
@@ -236,6 +236,12 @@
<artifactId>hdds-client</artifactId>
<scope>test</scope>
</dependency>
+ <dependency>
+ <groupId>org.apache.ozone</groupId>
+ <artifactId>hdds-client</artifactId>
+ <type>test-jar</type>
+ <scope>test</scope>
+ </dependency>
<dependency>
<groupId>org.apache.ozone</groupId>
<artifactId>hdds-common</artifactId>
diff --git
a/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/fs/ozone/TestOzoneFSInputStream.java
b/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/fs/ozone/TestOzoneFSInputStream.java
index e704f289770..9207fc42831 100644
---
a/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/fs/ozone/TestOzoneFSInputStream.java
+++
b/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/fs/ozone/TestOzoneFSInputStream.java
@@ -32,6 +32,7 @@
import java.net.URI;
import java.nio.ByteBuffer;
import java.nio.file.Files;
+import java.util.Arrays;
import java.util.UUID;
import java.util.stream.Stream;
import org.apache.commons.lang3.RandomStringUtils;
@@ -43,6 +44,7 @@
import org.apache.hadoop.hdds.client.ECReplicationConfig;
import org.apache.hadoop.hdds.conf.OzoneConfiguration;
import org.apache.hadoop.hdds.protocol.StorageType;
+import org.apache.hadoop.hdds.scm.storage.PositionedReadTestHelper;
import org.apache.hadoop.io.IOUtils;
import org.apache.hadoop.io.SequenceFile;
import org.apache.hadoop.ozone.DataTestUtil;
@@ -54,6 +56,7 @@
import org.apache.ozone.test.NonHATests;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
@@ -113,6 +116,41 @@ void shutdown() {
closeQuietly(client, ecFs);
}
+ @Test
+ void testConcurrentECPositionedRead() throws Exception {
+ Path path = new Path("/concurrent-ec-reads");
+ byte[] expected = Arrays.copyOf(data, 7 * 1024 * 1024 + 103);
+ try (FSDataOutputStream output = ecFs.create(path)) {
+ output.write(expected);
+ }
+ try (FSDataInputStream stream = ecFs.open(path)) {
+ stream.seek(123);
+ PositionedReadTestHelper.runConcurrentPositionedReads(expected,
stream::readFully);
+ assertEquals(123, stream.getPos());
+ }
+ }
+
+ @ParameterizedTest
+ @ValueSource(booleans = {true, false})
+ void testConcurrentPositionedRead(boolean streaming) throws Exception {
+ OzoneConfiguration conf = new OzoneConfiguration(cluster().getConf());
+ conf.setBoolean("ozone.client.stream.readblock.enable", streaming);
+ try (FileSystem fs = FileSystem.newInstance(URI.create(uri), conf);
+ FSDataInputStream stream = fs.open(filePath)) {
+ stream.seek(123);
+ PositionedReadTestHelper.runConcurrentPositionedReads(data, (offset,
buffer) -> {
+ if ((offset & 1) == 0) {
+ stream.readFully(offset, buffer);
+ } else {
+ byte[] bytes = new byte[buffer.remaining()];
+ stream.readFully(offset, bytes);
+ buffer.put(bytes);
+ }
+ });
+ assertEquals(123, stream.getPos());
+ }
+ }
+
@ParameterizedTest
@ValueSource(booleans = {true, false})
public void testO3FSSingleByteRead(boolean isStreamEnable) throws
IOException {
diff --git
a/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/client/rpc/TestOzoneAtRestEncryption.java
b/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/client/rpc/TestOzoneAtRestEncryption.java
index 3bf1df6b952..06314be3d2a 100644
---
a/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/client/rpc/TestOzoneAtRestEncryption.java
+++
b/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/client/rpc/TestOzoneAtRestEncryption.java
@@ -63,6 +63,7 @@
import java.util.function.BooleanSupplier;
import javax.xml.bind.DatatypeConverter;
import org.apache.commons.lang3.RandomUtils;
+import org.apache.hadoop.crypto.CryptoInputStream;
import org.apache.hadoop.crypto.key.JavaKeyStoreProvider;
import org.apache.hadoop.crypto.key.KeyProvider;
import org.apache.hadoop.crypto.key.KeyProviderCryptoExtension;
@@ -82,6 +83,7 @@
import org.apache.hadoop.hdds.scm.container.ContainerInfo;
import
org.apache.hadoop.hdds.scm.protocolPB.StorageContainerLocationProtocolClientSideTranslatorPB;
import org.apache.hadoop.hdds.scm.storage.MultipartInputStream;
+import org.apache.hadoop.hdds.scm.storage.PositionedReadTestHelper;
import
org.apache.hadoop.hdds.security.x509.certificate.client.CertificateClientTestImpl;
import org.apache.hadoop.hdds.utils.db.Table;
import org.apache.hadoop.ozone.ClientConfigForTesting;
@@ -308,6 +310,17 @@ void testPutKeyWithEncryption(BucketLayout bucketLayout)
throws Exception {
createAndVerifyKeyData(bucket);
createAndVerifyStreamKeyData(bucket);
createAndVerifyFileSystemData(bucket);
+
+ String keyName = UUID.randomUUID().toString();
+ byte[] data = generateRandomData(PositionedReadTestHelper.SOURCE_SIZE);
+ DataTestUtil.createKey(bucket, keyName,
ReplicationConfig.fromTypeAndFactor(RATIS, ONE), data);
+ try (OzoneInputStream inputStream = bucket.readKey(keyName)) {
+ assertInstanceOf(CryptoInputStream.class, inputStream.getInputStream());
+ inputStream.seek(123);
+ PositionedReadTestHelper.runConcurrentPositionedReads(data,
inputStream::readFully);
+ assertEquals(123, inputStream.getPos());
+ assertEquals(Byte.toUnsignedInt(data[123]), inputStream.read());
+ }
}
@ParameterizedTest
@@ -721,6 +734,19 @@ private void testMultipartUploadWithEncryption(OzoneBucket
bucket,
try (OzoneInputStream inputStream = bucket.readKey(keyName)) {
assertInstanceOf(MultipartInputStream.class,
inputStream.getInputStream());
+ if (numParts == 2) {
+ inputStream.seek(123);
+ PositionedReadTestHelper.runConcurrentPositionedReads(inputData,
inputStream::readFully);
+ int offset = partsData.get(0).length - 17;
+ ByteBuffer destination =
ByteBuffer.allocateDirect(DEFAULT_CRYPTO_BUFFER_SIZE + 37);
+ inputStream.readFully(offset, destination);
+ destination.flip();
+ byte[] actual = new byte[destination.remaining()];
+ destination.get(actual);
+ assertArrayEquals(Arrays.copyOfRange(inputData, offset, offset +
actual.length), actual);
+ assertEquals(123, inputStream.getPos());
+ inputStream.seek(0);
+ }
// Test complete read
byte[] completeRead = new byte[keySize];
diff --git
a/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/client/rpc/read/TestStreamRead.java
b/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/client/rpc/read/TestStreamRead.java
index 3ce0af44527..1322b52ced7 100644
---
a/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/client/rpc/read/TestStreamRead.java
+++
b/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/client/rpc/read/TestStreamRead.java
@@ -21,7 +21,7 @@
import static
org.apache.hadoop.ozone.OzoneConfigKeys.OZONE_CLIENT_BYTES_PER_CHECKSUM_MIN_SIZE;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.io.BufferedInputStream;
@@ -42,6 +42,7 @@
import
org.apache.hadoop.hdds.protocol.datanode.proto.ContainerProtos.ChunkInfo;
import org.apache.hadoop.hdds.scm.OzoneClientConfig;
import org.apache.hadoop.hdds.scm.ScmConfigKeys;
+import org.apache.hadoop.hdds.scm.storage.BlockInputStream;
import org.apache.hadoop.hdds.scm.storage.StreamBlockInputStream;
import org.apache.hadoop.hdds.utils.IOUtils;
import org.apache.hadoop.hdds.utils.db.CodecBuffer;
@@ -193,7 +194,7 @@ void testUnevenFlushedChunks() throws Exception {
long shiftedOffset = chunks.get(1).getOffset();
assertTrue(shiftedOffset % config.getBytesPerChecksum() != 0, "second
chunk must shift the checksum grid");
try (KeyInputStream in = bucket.getKeyInputStream(key)) {
- assertTrue(in.isStreamBlockInputStream());
+ in.getPartStreams().forEach(part ->
assertInstanceOf(StreamBlockInputStream.class, part));
byte[] actual = new byte[expected.length];
org.apache.hadoop.io.IOUtils.readFully(in, actual, 0, actual.length);
assertArrayEquals(expected, actual);
@@ -296,7 +297,7 @@ private static void
assertClientBytesPerChecksum(SizeInBytes configured, OzoneCl
static void streamRead(SizeInBytes keySize, SizeInBytes bufferSize, String
expectedMD5,
BucketForTesting bucket, String keyName) throws Exception {
try (KeyInputStream in = bucket.getKeyInputStream(keyName)) {
- assertTrue(in.isStreamBlockInputStream());
+ in.getPartStreams().forEach(part ->
assertInstanceOf(StreamBlockInputStream.class, part));
runTestReadKey(keySize, bufferSize, expectedMD5, in);
}
}
@@ -305,7 +306,7 @@ static void nonStreamRead(SizeInBytes keySize, SizeInBytes
bufferSize, String ex
OzoneClient nonStreamReadClient, String volume, String bucket, String
keyName) throws Exception {
final ClientProtocol proxy = nonStreamReadClient.getProxy();
try (KeyInputStream in = (KeyInputStream) proxy.getKey(volume, bucket,
keyName).getInputStream()) {
- assertFalse(in.isStreamBlockInputStream());
+ in.getPartStreams().forEach(part ->
assertInstanceOf(BlockInputStream.class, part));
runTestReadKey(keySize, bufferSize, expectedMD5, in);
}
}
diff --git
a/hadoop-ozone/ozonefs-common/src/main/java/org/apache/hadoop/fs/ozone/CapableOzoneFSInputStream.java
b/hadoop-ozone/ozonefs-common/src/main/java/org/apache/hadoop/fs/ozone/CapableOzoneFSInputStream.java
index f2e94bf2508..c53f8b73aa5 100644
---
a/hadoop-ozone/ozonefs-common/src/main/java/org/apache/hadoop/fs/ozone/CapableOzoneFSInputStream.java
+++
b/hadoop-ozone/ozonefs-common/src/main/java/org/apache/hadoop/fs/ozone/CapableOzoneFSInputStream.java
@@ -18,6 +18,7 @@
package org.apache.hadoop.fs.ozone;
import java.io.InputStream;
+import org.apache.hadoop.fs.ByteBufferPositionedReadable;
import org.apache.hadoop.fs.FileSystem.Statistics;
import org.apache.hadoop.fs.StreamCapabilities;
import org.apache.hadoop.util.StringUtils;
@@ -34,8 +35,9 @@ public boolean hasCapability(String capability) {
switch (StringUtils.toLowerCase(capability)) {
case StreamCapabilities.READBYTEBUFFER:
case StreamCapabilities.UNBUFFER:
- case StreamCapabilities.PREADBYTEBUFFER:
return true;
+ case StreamCapabilities.PREADBYTEBUFFER:
+ return getWrappedInputStream() instanceof ByteBufferPositionedReadable;
default:
return false;
}
diff --git
a/hadoop-ozone/ozonefs-common/src/main/java/org/apache/hadoop/fs/ozone/OzoneFSInputStream.java
b/hadoop-ozone/ozonefs-common/src/main/java/org/apache/hadoop/fs/ozone/OzoneFSInputStream.java
index 1b8e5e9c9db..cb6e551d65d 100644
---
a/hadoop-ozone/ozonefs-common/src/main/java/org/apache/hadoop/fs/ozone/OzoneFSInputStream.java
+++
b/hadoop-ozone/ozonefs-common/src/main/java/org/apache/hadoop/fs/ozone/OzoneFSInputStream.java
@@ -30,7 +30,6 @@
import org.apache.hadoop.fs.Seekable;
import org.apache.hadoop.hdds.annotation.InterfaceAudience;
import org.apache.hadoop.hdds.annotation.InterfaceStability;
-import org.apache.hadoop.hdds.scm.storage.ExtendedInputStream;
import org.apache.hadoop.hdds.tracing.TracingUtil;
/**
@@ -39,10 +38,7 @@
* Sequential reads are NOT thread safe.
* <p>
* Positioned reads are thread safe.
- * When the underlying stream is an {@link ExtendedInputStream} and
- * when it supports {@link ExtendedInputStream#readFully(long, ByteBuffer)},
- * they delegate to it.
- * Otherwise, they fall back to the default synchronized seek-read-restore
implementation.
+ * They delegate to the underlying {@link ByteBufferPositionedReadable} stream.
* <p>
* Applications must use either sequential reads or position reads at any
given time,
* but not concurrent sequential/position reads
@@ -54,7 +50,6 @@ public class OzoneFSInputStream extends FSInputStream
private final InputStream inputStream;
private final Statistics statistics;
- private final Object positionedReadLock = new Object();
public OzoneFSInputStream(InputStream inputStream, Statistics statistics) {
this.inputStream = inputStream;
@@ -140,15 +135,15 @@ private int readInTrace(ByteBuffer buf) throws
IOException {
int readLen = Math.min(buf.remaining(), available());
if (buf.hasArray()) {
int pos = buf.position();
- bytesRead = read(buf.array(), pos, readLen);
+ bytesRead = inputStream.read(buf.array(), buf.arrayOffset() + pos,
readLen);
if (bytesRead > 0) {
buf.position(pos + bytesRead);
}
} else {
byte[] readData = new byte[readLen];
- bytesRead = read(readData, 0, readLen);
+ bytesRead = inputStream.read(readData, 0, readLen);
if (bytesRead > 0) {
- buf.put(readData);
+ buf.put(readData, 0, bytesRead);
}
}
}
@@ -175,31 +170,35 @@ public void unbuffer() {
*/
@Override
public int read(long position, ByteBuffer buffer) throws IOException {
- if (!buffer.hasRemaining()) {
- return 0;
- }
if (position < 0) {
throw new EOFException("position is negative: " + position);
}
- return readImpl(position, buffer);
+ if (buffer.isReadOnly()) {
+ throw new ReadOnlyBufferException();
+ }
+ return buffer.hasRemaining() ? readImpl(position, buffer) : 0;
}
@Override
public int read(long position, byte[] array, int offset, int length) throws
IOException {
- if (length == 0) {
- return 0;
- }
validatePositionedReadArgs(position, array, offset, length);
- return readImpl(position, ByteBuffer.wrap(array, offset, length));
+ return read(position, ByteBuffer.wrap(array, offset, length));
+ }
+
+ protected InputStream getWrappedInputStream() {
+ return inputStream;
}
private int readImpl(long position, ByteBuffer buffer) throws IOException {
- final Integer n = bestEffortExtendedInputStreamRead(position, buffer,
false);
- if (n != null) {
- return n;
+ if (!(inputStream instanceof ByteBufferPositionedReadable)) {
+ throw new UnsupportedOperationException("Positioned reads are not
supported by "
+ + inputStream.getClass().getName());
+ }
+ final int n = ((ByteBufferPositionedReadable) inputStream).read(position,
buffer);
+ if (statistics != null && n > 0) {
+ statistics.incrementBytesRead(n);
}
- // Fallback: stateful seek-read-restore on the shared cursor.
- return bestEffortReadFullySynchronized(position, buffer);
+ return n;
}
/**
@@ -210,20 +209,11 @@ private int readImpl(long position, ByteBuffer buffer)
throws IOException {
*/
@Override
public void readFully(long position, ByteBuffer buffer) throws IOException {
- if (!buffer.hasRemaining()) {
- return;
- }
- if (position < 0) {
- throw new EOFException("position is negative: " + position);
- }
readFullyImpl(position, buffer);
}
@Override
public void readFully(long position, byte[] array, int offset, int length)
throws IOException {
- if (length == 0) {
- return;
- }
validatePositionedReadArgs(position, array, offset, length);
readFullyImpl(position, ByteBuffer.wrap(array, offset, length));
}
@@ -234,94 +224,15 @@ public void readFully(long position, byte[] buffer)
throws IOException {
}
private void readFullyImpl(long position, ByteBuffer buffer) throws
IOException {
- final int length = buffer.remaining();
- final Integer n = bestEffortExtendedInputStreamRead(position, buffer,
true);
- if (n != null) {
- if (n < length) {
- throw new EOFException("Failed to read fully length " + length + " at
position " + position);
- }
- return;
- }
- final int m = bestEffortReadFullySynchronized(position, buffer);
- if (m < length) {
- throw new EOFException("Failed to read fully length " + length + " at
position " + position);
- }
- }
-
- /**
- * Best effort read via {@link ExtendedInputStream#readFully(long,
ByteBuffer)}.
- *
- * @return the number of bytes read; or null when the native stateless path
is unsupported.
- */
- private Integer bestEffortExtendedInputStreamRead(long position, ByteBuffer
buffer,
- boolean isReadFully) throws IOException {
- if (!(inputStream instanceof ExtendedInputStream)) {
- return null; // not an ExtendedInputStream
- }
- final int remainingBeforeRead = buffer.remaining();
- try {
- if (!((ExtendedInputStream) inputStream).readFully(position, buffer)) {
- return null; // ExtendedInputStream does not support readFully
+ do {
+ final int n = read(position, buffer);
+ if (n < 0) {
+ throw new EOFException("End of stream at position " + position);
}
- } catch (EOFException e) {
- if (isReadFully) {
- throw e;
+ if (n == 0 && buffer.hasRemaining()) {
+ throw new IOException("No progress reading at position " + position);
}
- // read() semantics: EOF -> -1 (fall through to bytesRead == 0 check
below)
- }
- final int bytesRead = remainingBeforeRead - buffer.remaining();
- if (bytesRead == 0) {
- return -1;
- }
- if (statistics != null) {
- statistics.incrementBytesRead(bytesRead);
- }
- return bytesRead;
- }
-
- /**
- * Fallback positioned read via synchronized seek-read-restore.
- * Uses {@link #read(ByteBuffer)} -- which handles streams that do not
implement
- * {@link org.apache.hadoop.fs.ByteBufferReadable} (e.g. GDPR
- * {@code javax.crypto.CipherInputStream}) -- so this works for any {@link
Seekable}
- * inner stream.
- *
- * @return the number of bytes read
- */
- private int bestEffortReadFullySynchronized(long position, ByteBuffer
buffer) throws IOException {
- synchronized (positionedReadLock) {
- final long oldPos = getPos();
- try {
- ((Seekable) inputStream).seek(position);
- final int n = bestEffortRead(buffer);
- if (statistics != null) {
- statistics.incrementBytesRead(n);
- }
- return n;
- } finally {
- ((Seekable) inputStream).seek(oldPos);
- }
- }
- }
-
- /**
- * Best effort read to fill up the buffer using {@link #read(ByteBuffer)}.
- *
- * @return the number of bytes read
- */
- private int bestEffortRead(ByteBuffer buffer) throws IOException {
- int readLength = 0;
- try {
- while (buffer.hasRemaining()) {
- final int n = read(buffer);
- if (n < 0) {
- return readLength;
- }
- readLength += n;
- }
- return readLength;
- } catch (EOFException e) {
- return readLength;
- }
+ position += n;
+ } while (buffer.hasRemaining());
}
}
diff --git
a/hadoop-ozone/ozonefs-common/src/test/java/org/apache/hadoop/fs/ozone/TestOzoneFSInputStream.java
b/hadoop-ozone/ozonefs-common/src/test/java/org/apache/hadoop/fs/ozone/TestOzoneFSInputStream.java
index 6954547e739..1caa968142a 100644
---
a/hadoop-ozone/ozonefs-common/src/test/java/org/apache/hadoop/fs/ozone/TestOzoneFSInputStream.java
+++
b/hadoop-ozone/ozonefs-common/src/test/java/org/apache/hadoop/fs/ozone/TestOzoneFSInputStream.java
@@ -20,6 +20,7 @@
import static
org.apache.hadoop.hdds.scm.storage.PositionedReadTestHelper.SOURCE_SIZE;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.anyString;
@@ -34,33 +35,34 @@
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
+import java.nio.ReadOnlyBufferException;
import java.security.GeneralSecurityException;
import java.util.Arrays;
import java.util.List;
import java.util.function.IntFunction;
+import javax.crypto.CipherInputStream;
+import javax.crypto.NullCipher;
import org.apache.commons.lang3.RandomUtils;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.crypto.CipherSuite;
import org.apache.hadoop.crypto.CryptoCodec;
import org.apache.hadoop.crypto.CryptoInputStream;
import org.apache.hadoop.crypto.Decryptor;
+import org.apache.hadoop.fs.ByteBufferPositionedReadable;
import org.apache.hadoop.fs.FileSystem;
+import org.apache.hadoop.fs.PositionedReadable;
import org.apache.hadoop.fs.Seekable;
import org.apache.hadoop.fs.StreamCapabilities;
-import org.apache.hadoop.hdds.scm.storage.ByteReaderStrategy;
-import org.apache.hadoop.hdds.scm.storage.ExtendedInputStream;
import org.apache.hadoop.hdds.scm.storage.PositionedReadTestHelper;
import org.apache.hadoop.ozone.client.io.KeyInputStream;
+import org.apache.hadoop.ozone.client.io.OzoneInputStream;
import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.Timeout;
/**
* Tests for {@link OzoneFSInputStream}.
*/
public class TestOzoneFSInputStream {
- private static final byte CORRUPT_BYTE = (byte) 0x5A;
-
private static final List<IntFunction<ByteBuffer>> BUFFER_CONSTRUCTORS =
ImmutableList.of(ByteBuffer::allocate, ByteBuffer::allocateDirect);
@@ -198,193 +200,148 @@ public int read() {
}
@Test
- public void testByteBufferPositionedReadNegativePositionThrows() throws
Exception {
- // read(long, ByteBuffer) must throw EOFException for negative positions,
- // aligning with the byte-array PositionedReadable behaviour.
- final byte[] source = RandomUtils.secure().randomBytes(SOURCE_SIZE);
- final InterleavingSeekableInputStream underlying =
- new InterleavingSeekableInputStream(source);
- try (OzoneFSInputStream subject = new OzoneFSInputStream(underlying,
- new FileSystem.Statistics("test"))) {
- ByteBuffer buf = ByteBuffer.allocate(16);
- assertThrows(EOFException.class, () -> subject.read(-1L, buf));
- }
- }
-
- @Test
- @Timeout(value = 30)
- public void testByteArrayFallbackWorksForSeekableOnlyStream() throws
Exception {
- // Regression: the old routing through read(long, ByteBuffer) would cast to
- // ByteBufferReadable in readAtPositionSeekRestore and throw
ClassCastException
- // for a stream that only implements Seekable (not ByteBufferReadable).
- final byte[] source = RandomUtils.secure().randomBytes(SOURCE_SIZE);
- final SeekableOnlyInputStream underlying = new
SeekableOnlyInputStream(source);
- try (OzoneFSInputStream subject = new OzoneFSInputStream(underlying,
- new FileSystem.Statistics("test"))) {
- PositionedReadTestHelper.runConcurrentPositionedReads(source, (offset,
buf) -> {
- byte[] arr = new byte[buf.remaining()];
- subject.readFully(offset, arr);
- buf.put(arr);
- });
+ void cursorOnlyStreamsDoNotEmulatePositionedReads() throws Exception {
+ byte[] source = RandomUtils.secure().randomBytes(32);
+ try (CapableOzoneFSInputStream fs = new CapableOzoneFSInputStream(new
SeekableOnlyInputStream(source), null);
+ OzoneInputStream client = new OzoneInputStream(new
SeekableOnlyInputStream(source))) {
+ assertFalse(fs.hasCapability(StreamCapabilities.PREADBYTEBUFFER));
+ assertFalse(client.hasCapability(StreamCapabilities.PREADBYTEBUFFER));
+ assertFalse(client.hasCapability(StreamCapabilities.READBYTEBUFFER));
+ assertFalse(client.hasCapability(StreamCapabilities.UNBUFFER));
+ assertThrows(EOFException.class, () -> fs.read(-1,
ByteBuffer.allocate(1)));
+ assertThrows(UnsupportedOperationException.class, () -> fs.read(0,
ByteBuffer.allocate(1)));
+ assertThrows(UnsupportedOperationException.class, () -> client.read(0,
ByteBuffer.allocate(1)));
+ assertEquals(0, fs.getPos());
+ assertEquals(0, client.getPos());
+ assertEquals(Byte.toUnsignedInt(source[0]), fs.read());
+ assertEquals(Byte.toUnsignedInt(source[0]), client.read());
}
}
@Test
- @Timeout(value = 30)
- public void testConcurrentPositionedRead() throws Exception {
- final byte[] source = RandomUtils.secure().randomBytes(SOURCE_SIZE);
- final InterleavingSeekableInputStream underlying =
- new InterleavingSeekableInputStream(source);
- try (OzoneFSInputStream subject = new OzoneFSInputStream(underlying,
- new FileSystem.Statistics("test"))) {
- PositionedReadTestHelper.runConcurrentPositionedReads(source,
- (offset, buf) -> subject.readFully(offset, buf));
+ void gdprDelegateDoesNotSupportPositionedReads() throws Exception {
+ byte[] data = new byte[] {1, 2, 3};
+ try (OzoneInputStream client = new OzoneInputStream(new
CipherInputStream(new ByteArrayInputStream(data),
+ new NullCipher()));
+ CapableOzoneFSInputStream fs = new
CapableOzoneFSInputStream(client.getInputStream(), null)) {
+ assertFalse(fs.hasCapability(StreamCapabilities.PREADBYTEBUFFER));
+ assertThrows(UnsupportedOperationException.class, () -> fs.read(0, new
byte[1], 0, 1));
+ byte[] actual = new byte[data.length];
+ assertEquals(data.length, fs.read(actual));
+ assertArrayEquals(data, actual);
}
}
@Test
- @Timeout(value = 30)
- public void testConcurrentPositionedReadEcFallback() throws Exception {
- final byte[] source = RandomUtils.secure().randomBytes(SOURCE_SIZE);
- final EcInterleavingInputStream underlying =
- new EcInterleavingInputStream(source);
- try (OzoneFSInputStream subject = new OzoneFSInputStream(underlying,
- new FileSystem.Statistics("test"))) {
- PositionedReadTestHelper.runConcurrentPositionedReads(source,
- (offset, buf) -> subject.readFully(offset, buf));
+ void positionedReadsValidateEmptyRequests() throws Exception {
+ byte[] data = new byte[8];
+ try (OzoneInputStream client = new OzoneInputStream(new
NativePositionedInputStream(data));
+ OzoneFSInputStream fs = createTestSubject(client)) {
+ for (ByteBufferPositionedReadable stream : Arrays.asList(client, fs)) {
+ ByteBuffer empty = ByteBuffer.allocate(0);
+ ByteBuffer readOnly = empty.asReadOnlyBuffer();
+ assertThrows(EOFException.class, () -> stream.read(-1, empty));
+ assertThrows(EOFException.class, () -> stream.readFully(-1, empty));
+ assertThrows(ReadOnlyBufferException.class, () -> stream.read(0,
readOnly));
+ assertThrows(ReadOnlyBufferException.class, () -> stream.readFully(0,
readOnly));
+ assertThrows(NullPointerException.class, () -> stream.read(0, null));
+ assertThrows(NullPointerException.class, () -> stream.readFully(0,
null));
+ PositionedReadable arrayStream = (PositionedReadable) stream;
+ assertThrows(EOFException.class, () -> arrayStream.read(-1, data, 0,
0));
+ assertThrows(EOFException.class, () -> arrayStream.readFully(-1, data,
0, 0));
+ assertThrows(IllegalArgumentException.class, () -> arrayStream.read(0,
null, 0, 0));
+ assertThrows(IllegalArgumentException.class, () ->
arrayStream.readFully(0, null, 0, 0));
+ assertThrows(IndexOutOfBoundsException.class, () ->
arrayStream.read(0, data, -1, 0));
+ assertThrows(IndexOutOfBoundsException.class, () ->
arrayStream.readFully(0, data, -1, 0));
+ assertThrows(IndexOutOfBoundsException.class, () ->
arrayStream.read(0, data, data.length + 1, 0));
+ assertThrows(IndexOutOfBoundsException.class, () ->
arrayStream.readFully(0, data, data.length + 1, 0));
+ for (long position : new long[] {0, data.length, data.length + 1}) {
+ assertEquals(0, stream.read(position, empty));
+ stream.readFully(position, empty);
+ assertEquals(0, arrayStream.read(position, data, data.length, 0));
+ arrayStream.readFully(position, data, data.length, 0);
+ }
+ }
}
}
@Test
- @Timeout(value = 30)
- public void testConcurrentByteArrayPositionedReadEcFallback() throws
Exception {
- final byte[] source = RandomUtils.secure().randomBytes(SOURCE_SIZE);
- final EcInterleavingInputStream underlying =
- new EcInterleavingInputStream(source);
- try (OzoneFSInputStream subject = new OzoneFSInputStream(underlying,
- new FileSystem.Statistics("test"))) {
- PositionedReadTestHelper.runConcurrentPositionedReads(source, (offset,
buf) -> {
- byte[] arr = new byte[buf.remaining()];
- subject.readFully(offset, arr);
- buf.put(arr);
- });
+ void clientRespectsDelegateCapabilities() throws Exception {
+ KeyInputStream key = mock(KeyInputStream.class);
+ when(key.hasCapability(anyString())).thenReturn(false);
+ try (OzoneInputStream client = new OzoneInputStream(key)) {
+ assertFalse(client.hasCapability(StreamCapabilities.PREADBYTEBUFFER));
+ assertFalse(client.hasCapability(StreamCapabilities.READBYTEBUFFER));
+ assertFalse(client.hasCapability(StreamCapabilities.UNBUFFER));
}
}
@Test
- @Timeout(value = 30)
- public void testConcurrentMixedApiEcFallback() throws Exception {
- // ByteBuffer and byte-array callers share positionedReadLock; verify no
interleaving.
- final byte[] source = RandomUtils.secure().randomBytes(SOURCE_SIZE);
- final EcInterleavingInputStream underlying =
- new EcInterleavingInputStream(source);
- try (OzoneFSInputStream subject = new OzoneFSInputStream(underlying,
- new FileSystem.Statistics("test"))) {
- PositionedReadTestHelper.runConcurrentPositionedReads(source, (offset,
buf) -> {
+ void concurrentPositionedReadsThroughWrappers() throws Exception {
+ byte[] source = RandomUtils.secure().randomBytes(SOURCE_SIZE);
+ try (OzoneFSInputStream stream = createTestSubject(new
OzoneInputStream(new NativePositionedInputStream(source)))) {
+ PositionedReadTestHelper.runConcurrentPositionedReads(source, (offset,
buffer) -> {
if ((offset & 1) == 0) {
- subject.readFully(offset, buf);
+ stream.readFully(offset, buffer);
} else {
- byte[] arr = new byte[buf.remaining()];
- subject.readFully(offset, arr);
- buf.put(arr);
+ byte[] bytes = new byte[buffer.remaining()];
+ stream.readFully(offset, bytes);
+ buffer.put(bytes);
}
});
}
}
- /**
- * Mimics KeyInputStream synchronized per-operation seek/read where
multi-steps
- * positioned reads must still be serialized at the FS layer.
- */
- private static final class InterleavingSeekableInputStream extends
InputStream
- implements Seekable, org.apache.hadoop.fs.ByteBufferReadable {
-
- private final InterleavingReadState readState;
-
- private InterleavingSeekableInputStream(byte[] data) {
- this.readState = new InterleavingReadState(data);
- }
-
- @Override
- public synchronized void seek(long p) {
- readState.seek(p);
- }
-
- @Override
- public synchronized long getPos() {
- return readState.getPos();
- }
-
- @Override
- public synchronized boolean seekToNewSource(long targetPos) {
- return false;
- }
-
- @Override
- public int read() {
- return -1;
- }
-
- @Override
- public synchronized int read(ByteBuffer buf) {
- return readState.read(buf);
+ @Test
+ void positionedReadDelegatesThroughWrappersAndCountsBytesOnce() throws
Exception {
+ byte[] source = RandomUtils.secure().randomBytes(32);
+ for (boolean wrapped : new boolean[] {false, true}) {
+ FileSystem.Statistics statistics = new FileSystem.Statistics("test");
+ InputStream input = new NativePositionedInputStream(source);
+ if (wrapped) {
+ input = new OzoneInputStream(input);
+ }
+ try (OzoneFSInputStream stream = new OzoneFSInputStream(input,
statistics)) {
+ byte[] result = new byte[12];
+ stream.readFully(3, result, 2, 7);
+ assertArrayEquals(Arrays.copyOfRange(source, 3, 10),
Arrays.copyOfRange(result, 2, 9));
+ assertEquals(7, statistics.getBytesRead());
+ ByteBuffer destination = ByteBuffer.wrap(result, 2, 7).slice();
+ assertEquals(3, stream.read(29, destination));
+ assertArrayEquals(Arrays.copyOfRange(source, 29, 32),
Arrays.copyOfRange(result, 2, 5));
+ assertEquals(10, statistics.getBytesRead());
+ assertEquals(-1, stream.read(32, ByteBuffer.allocateDirect(1)));
+ assertThrows(EOFException.class, () -> stream.readFully(31,
ByteBuffer.allocateDirect(2)));
+ assertEquals(11, statistics.getBytesRead());
+ }
}
}
- /**
- * Mimics an erasure-coded key stream: {@link ExtendedInputStream#readFully}
- * returns {@code false}, so {@link OzoneFSInputStream} falls back to
- * seek-read-restore on the shared cursor.
- */
- private static final class EcInterleavingInputStream extends
ExtendedInputStream {
-
- private final InterleavingReadState readState;
-
- private EcInterleavingInputStream(byte[] data) {
- this.readState = new InterleavingReadState(data);
- }
-
- @Override
- protected int readWithStrategy(ByteReaderStrategy strategy) {
- throw new UnsupportedOperationException();
- }
-
- @Override
- public synchronized void seek(long p) {
- readState.seek(p);
+ private static final class NativePositionedInputStream extends
ByteArrayInputStream
+ implements ByteBufferPositionedReadable {
+ private NativePositionedInputStream(byte[] data) {
+ super(data);
}
@Override
- public synchronized long getPos() {
- return readState.getPos();
- }
-
- @Override
- public synchronized boolean seekToNewSource(long targetPos) {
- return false;
- }
-
- @Override
- public synchronized int read(ByteBuffer buf) {
- return readState.read(buf);
- }
-
- @Override
- public synchronized int read(byte[] b, int off, int len) {
- return readState.read(b, off, len);
+ public int read(long position, ByteBuffer destination) {
+ if (position >= count) {
+ return -1;
+ }
+ int n = Math.min(destination.remaining(), count - (int) position);
+ destination.put(buf, (int) position, n);
+ return n;
}
@Override
- public void unbuffer() {
- return;
+ public void readFully(long position, ByteBuffer destination) throws
IOException {
+ int length = destination.remaining();
+ if (read(position, destination) < length) {
+ throw new EOFException();
+ }
}
}
- /**
- * A Seekable stream that does NOT implement ByteBufferReadable. Used to
verify
- * that the byte-array positioned-read fallback uses read(byte[]) rather than
- * casting to ByteBufferReadable (which would throw ClassCastException).
- */
private static final class SeekableOnlyInputStream extends InputStream
implements Seekable {
@@ -432,58 +389,4 @@ public boolean seekToNewSource(long targetPos) {
}
}
- private static final class InterleavingReadState {
- private final byte[] data;
- private long pos;
- private final ThreadLocal<Long> expectedReadPos = new ThreadLocal<>();
-
- private InterleavingReadState(byte[] data) {
- this.data = data;
- }
-
- private void seek(long p) {
- pos = p;
- expectedReadPos.set(p);
- }
-
- private long getPos() {
- return pos;
- }
-
- private int read(ByteBuffer buf) {
- Long expected = expectedReadPos.get();
- if (expected != null && pos != expected) {
- int len = buf.remaining();
- for (int i = 0; i < len; i++) {
- buf.put(CORRUPT_BYTE);
- }
- return len;
- }
- int toRead = Math.min(buf.remaining(), data.length - (int) pos);
- if (toRead <= 0) {
- return -1;
- }
- buf.put(data, (int) pos, toRead);
- pos += toRead;
- expectedReadPos.remove();
- return toRead;
- }
-
- private int read(byte[] b, int off, int len) {
- Long expected = expectedReadPos.get();
- if (expected != null && pos != expected) {
- java.util.Arrays.fill(b, off, off + len, CORRUPT_BYTE);
- return len;
- }
- int toRead = Math.min(len, data.length - (int) pos);
- if (toRead <= 0) {
- return -1;
- }
- System.arraycopy(data, (int) pos, b, off, toRead);
- pos += toRead;
- expectedReadPos.remove();
- return toRead;
- }
- }
-
}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]