yandrey321 commented on code in PR #11348:
URL: https://github.com/apache/ozone/pull/11348#discussion_r4188486706
##########
hadoop-hdds/client/src/main/java/org/apache/hadoop/hdds/scm/storage/MultipartInputStream.java:
##########
@@ -195,45 +180,35 @@ 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) {
Review Comment:
Two problems, both from this method being unsynchronized while the state it
reads is
mutable.
(a) Visibility. `length` (line 37) is neither final nor volatile. It is
written in the
constructor and again in `synchronized initialize()` (line 242). A
positioned read
racing a first sequential op can see either value with no happens-before
edge. Making
`length` volatile is the minimal fix and costs nothing on this path.
(`closed` was
correctly made volatile in this patch — `length` needs the same treatment.)
(b) The clamp can go negative. `partOffsets` is computed once in the
constructor from
pre-initialize part lengths and is never recomputed, while `initialize()`
recomputes
`length` and, for under-construction blocks,
BlockInputStream.initialize():167 resets
`length = blockData.getSize()` from what the datanode actually has. If a
part's length
comes back *shorter* than the OM-reported length the offsets were built
from, then for
a `pos` that is still < the new `length`, `part.getLength() - partPosition`
is
negative and `buffer.limit(buffer.position() + negative)` throws
IllegalArgumentException — a RuntimeException escaping a read path that
callers only
guard for IOException. (The offset staleness itself predates this PR; the
sequential
path at lines 149-155/178 shares it. What is new is that the clamp converts
it into an
unchecked throw.)
Both are cheap to close:
final long available = part.getLength() - partPosition;
if (available <= 0) {
break; // offsets and lengths disagree; treat as EOF
}
buffer.limit(buffer.position() + (int) Math.min(buffer.remaining(),
available));
and recomputing `partOffsets` in initialize() (or documenting on the field
that it is
only valid for a fully-committed key) would make the invariant explicit
rather than
implicit.
##########
hadoop-ozone/ozonefs-common/src/main/java/org/apache/hadoop/fs/ozone/CapableOzoneFSInputStream.java:
##########
@@ -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:
Review Comment:
This check is a tautology in production. The wrapped stream here is always an
OzoneInputStream (built in RpcClient.createInputStream and handed to
CapableOzoneFSInputStream by OzoneFileSystem:114 /
RootedOzoneFileSystem:112), and
this PR makes OzoneInputStream extend ExtendedInputStream, which *implements*
ByteBufferPositionedReadable unconditionally. So the answer is `true` for
every key,
including a GDPR key, where RpcClient:2682 wraps a
javax.crypto.CipherInputStream and
OzoneInputStream.readPositioned:83 throws UnsupportedOperationException.
A caller that does the right thing — probe hasCapability(PREADBYTEBUFFER),
then call
read(long, ByteBuffer) — gets told yes and then gets a RuntimeException.
OzoneInputStream.hasCapability already computes this correctly (it tests its
own
delegate). Delegate to it instead of re-deriving from the type:
case StreamCapabilities.PREADBYTEBUFFER:
return getWrappedInputStream() instanceof StreamCapabilities
&& ((StreamCapabilities)
getWrappedInputStream()).hasCapability(capability);
Note the new unit test does not catch this because it bypasses the production
wrapping: cursorOnlyStreamsDoNotEmulatePositionedReads constructs
`new CapableOzoneFSInputStream(new SeekableOnlyInputStream(source), null)` —
a raw
delegate rather than an OzoneInputStream — so the instanceof legitimately
answers
false there. Please add a case that wraps the delegate in OzoneInputStream,
which is
the only shape the filesystem ever sees.
##########
hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/io/OzoneInputStream.java:
##########
@@ -66,6 +70,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 inputStream instanceof CanUnbuffer;
Review Comment:
This is the fall-through for "whatever capability the base class said true
to that
isn't one of the two above" — correct only because the base switch has
exactly three
cases. If ExtendedInputStream ever adds a fourth, this silently answers the
wrong
question. Make it explicit:
if (StreamCapabilities.UNBUFFER.equalsIgnoreCase(capability)) {
return inputStream instanceof CanUnbuffer;
}
return false;
##########
hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/io/OzoneInputStream.java:
##########
@@ -66,6 +70,38 @@ public int read(ByteBuffer byteBuffer) throws IOException {
}
}
+ @Override
+ protected int readWithStrategy(ByteReaderStrategy strategy) throws
IOException {
Review Comment:
This is dead code: read(), read(byte[],int,int) and read(ByteBuffer) at
:53-:71 all
delegate straight to `inputStream` and never reach the base class's
`read(ByteReaderStrategy)`. Two consequences worth deciding on deliberately:
- the base class's sequential entry points are `synchronized`, these
overrides are
not, so OzoneInputStream's sequential reads have different locking from
every
other ExtendedInputStream;
- if the strategy path is genuinely unreachable, implementing it invites a
future
reader to assume it is live.
Either drop the direct overrides and let the strategy path do the work
(uniform
locking, one code path), or keep them and make readWithStrategy throw
UnsupportedOperationException with a comment saying why.
##########
hadoop-hdds/client/src/main/java/org/apache/hadoop/hdds/scm/storage/MultipartInputStream.java:
##########
@@ -195,45 +180,35 @@ 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 oldLimit = buffer.limit();
+ final int n;
+ try {
+ buffer.limit(buffer.position() + (int) Math.min(buffer.remaining(),
part.getLength() - partPosition));
+ n = part.read(partPosition, buffer);
+ } finally {
+ buffer.limit(oldLimit);
+ }
+ 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) {
Review Comment:
binarySearchOffsetIndex relies on Arrays.binarySearch, whose result among
*duplicate*
offsets is unspecified. A zero-length part produces a duplicate offset, so
the search
can land on it; the clamp at line 197 then sets the limit to the current
position,
part.read() correctly returns 0 for an empty buffer, and this throws — for a
stream
that is in no way broken. The method created the empty buffer itself, so 0
here is not
a protocol violation.
`continue` (after advancing past the part) rather than throwing would be
correct. Same
`available <= 0` guard suggested above covers it.
I could not construct a zero-length part from today's write paths, so this is
robustness rather than a live bug — but the throw is reachable from a state
the method
itself produces, which is worth closing cheaply.
##########
hadoop-hdds/client/src/main/java/org/apache/hadoop/ozone/client/io/ECBlockInputStreamProxy.java:
##########
@@ -197,6 +197,23 @@ 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, reconstruction buffers and failure
recovery.
+ try (ECBlockInputStreamProxy reader = new
ECBlockInputStreamProxy(repConfig, blockInfo,
Review Comment:
Correctness is fine and the new test proves readers are closed and reads
overlap. The
concern is what this costs per call. Each positioned read constructs and
tears down a
complete EC reader: on the direct path, data-count BlockInputStreams with
their own
xceiver clients; on the reconstruction path, a stripe reader holding
ByteBufferPool
buffers. read(ByteBuffer) does loop internally, so it is one reader per
block per
pread rather than per chunk — but for a 4KB pread against a 3-2 EC key that
is still
three fresh datanode streams built and discarded.
More importantly, the fresh proxy starts with an empty `failedLocations`. A
replica
that the outer stream has already failed over on (BadDataLocationException)
is
re-attempted by every subsequent positioned read, so each one pays the
connect/read
timeout to the dead datanode again before falling back to reconstruction.
Under a
positioned-read workload against a key with one bad replica, that is a
per-call
penalty that never amortizes.
Worth at least a comment acknowledging it, and ideally passing the outer
`failedLocations` into the per-request reader (the ECBlockInputStreamFactory
signature
already takes a failed-locations list). If sharing the set across threads is
the
obstacle, a CopyOnWriteArrayList or a snapshot-plus-merge would do.
##########
hadoop-ozone/ozonefs-common/src/main/java/org/apache/hadoop/fs/ozone/OzoneFSInputStream.java:
##########
@@ -193,13 +188,23 @@ public int read(long position, byte[] array, int offset,
int length) throws IOEx
return readImpl(position, ByteBuffer.wrap(array, offset, length));
}
+ protected InputStream getWrappedInputStream() {
+ return inputStream;
+ }
+
private int readImpl(long position, ByteBuffer buffer) throws IOException {
Review Comment:
Both byte-array overloads now route here — read(long, byte[], int, int):183
and
readFully(long, byte[], int, int):228 — so they throw for any delegate that
cannot do
ByteBuffer positioned reads. That is a functional regression, not just a
capability
question: read(long, byte[], int, int) and readFully are plain
PositionedReadable
methods. Hadoop callers invoke them with no capability probe at all
(FSDataInputStream
exposes them directly; ORC, Parquet and Hadoop's own IOUtils.readFully use
them), and
there is no StreamCapabilities constant that gates them.
Before this patch, bestEffortRead/bestEffortReadFullySynchronized served
these under
positionedReadLock via seek-read-restore. The javadoc you deleted named
exactly the
case that now breaks:
"Uses read(ByteBuffer) -- which handles streams that do not implement
ByteBufferReadable (e.g. GDPR javax.crypto.CipherInputStream)"
Reaching concurrency by deleting the only path that worked for a whole class
of keys
is the wrong trade. Options, in order of preference:
a) Keep the locked seek-read-restore fallback for the byte-array overloads
only,
reached when the delegate is not ByteBufferPositionedReadable.
Concurrency is
unchanged for every stream that *does* support native pread; GDPR keys
keep
working, serialized, as they do today.
b) Make the GDPR path capable at the source — RpcClient:2682 could wrap the
KeyInputStream in something that implements
ByteBufferPositionedReadable with
per-call Cipher instances — which is a larger change and probably its
own Jira.
Either way the PR description should say that GDPR-encrypted keys are
affected; right
now it reads as purely additive.
##########
hadoop-hdds/client/src/main/java/org/apache/hadoop/hdds/scm/storage/ExtendedInputStream.java:
##########
@@ -100,6 +128,7 @@ public synchronized boolean seekToNewSource(long l) throws
IOException {
@Override
public boolean hasCapability(String capability) {
Review Comment:
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:
// ECBlockInputStream:xxx and ECBlockReconstructedInputStream:xxx
return !StreamCapabilities.PREADBYTEBUFFER.equalsIgnoreCase(capability)
&& super.hasCapability(capability);
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 EC
classes return false from, with the switch consulting it — than as a default
that two
classes already have to undo.
##########
hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/io/OzoneInputStream.java:
##########
@@ -66,6 +70,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 {
Review Comment:
The no-arg constructor at :40 leaves `inputStream` null, so this NPEs on
`getClass()` instead of throwing the intended UnsupportedOperationException.
A
null-guard in the message (or in the constructor) is a one-liner.
##########
hadoop-ozone/client/src/main/java/org/apache/hadoop/ozone/client/io/OzoneInputStream.java:
##########
@@ -20,15 +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,
+public class OzoneInputStream extends ExtendedInputStream implements
CanUnbuffer,
Review Comment:
- The `implements` clause is now entirely redundant — ExtendedInputStream
already
implements all three.
- This changes the superclass of a public client API type (returned by
OzoneBucket.readKey()) from java.io.InputStream to
org.apache.hadoop.fs.FSInputStream. It stays binary- and source-compatible
(InputStream remains an ancestor, and the added members are all new), but
it does
put hadoop-common's fs hierarchy into the public client type and adds a
dozen
public methods to it. Worth a line in the PR description so reviewers can
weigh it
rather than discover it.
##########
hadoop-ozone/ozonefs-common/src/main/java/org/apache/hadoop/fs/ozone/OzoneFSInputStream.java:
##########
Review Comment:
ExtendedInputStream:45 checks these in the opposite order, so `read(-1,
emptyBuffer)`
returns 0 here and throws EOFException there. Likewise read(long, byte[],
int, int):184
returns 0 for `length == 0` *before* validatePositionedReadArgs, which skips
the
null-buffer and negative-position checks that Hadoop's contract specifies.
Low stakes, but the two layers should agree, and validating first is the
cheaper
convention to defend. The new test only exercises the negative position with
a
non-empty buffer, so the divergence is currently untested.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]