Repository navigation
HDDS-16396. Support concurrent positioned reads for EC and encrypted keys #11348
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
c5b1ec4
5f3141d
a300470
0859cf2
242ef5d
3513eb2
0a5b8af
7fbc445
b81b346
a78b33b
2d2e4c7
27fcefe
44f9ebf
04c2c04
cb65829
105478c
26e95e6
a4ebc7f
834dce8
7b6b7e4
deee688
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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) { | ||
|
peterxcli marked this conversation as resolved.
|
||
| 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) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. binarySearchOffsetIndex relies on Arrays.binarySearch, whose result among duplicate
I could not construct a zero-length part from today's write paths, so this is
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The write paths omit zero-length blocks when committing to OM, so we don't need an empty-part guard here. I've removed the buffer clamp and left bounds handling to each part. |
||
| 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) { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -18,32 +18,21 @@ | |
| 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 { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. with ByteBufferPositionedReadable, that looks better. |
||
| long getLength(); | ||
|
|
||
| default long getRemaining() throws IOException { | ||
| return getLength() - getPos(); | ||
| } | ||
|
|
||
| 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()); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Advertising PREADBYTEBUFFER in the base class means every present and future
ExtendedInputStream claims it by default, and a subclass that cannot honour it has to
remember to say so. Two already have to, and the resulting override is hard to read:
The previous shape (
readFully(long, ByteBuffer)returning false until overridden)was opt-in, which fails safe. Given that finding #1 above is exactly a
false-positive capability report, I would rather see this as an overridable hook —
e.g. a
protected boolean supportsPositionedRead() { return true; }that the two ECclasses return false from, with the switch consulting it — than as a default that two
classes already have to undo.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The proposed hook still defaults to true, so unsupported streams would still need to opt out. I'd keep the existing overrides.