Skip to content

HDDS-15734. Implementing position read in BlockInputStream#10765

Open
chungen0126 wants to merge 8 commits into
apache:masterfrom
chungen0126:HDDS-15734
Open

HDDS-15734. Implementing position read in BlockInputStream#10765
chungen0126 wants to merge 8 commits into
apache:masterfrom
chungen0126:HDDS-15734

Conversation

@chungen0126

@chungen0126 chungen0126 commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

What changes were proposed in this pull request?

Summary

To resolve the significant lock contention experienced during concurrent readVectored operations, this PR implements the ByteBufferPositionedReadable interface for both BlockInputStream and ChunkInputStream. By supporting thread-safe, position-based reads directly into ByteBuffer, we bypass the synchronization overhead of traditional stream locks and greatly improve parallel read performance.

Detailed Changes

  1. Improve Visibility of Initialization State
  • Declared the initialized flag as volatile. This ensures that once the stream is initialized, the state change is instantly visible to all other concurrent threads, preventing redundant initialization.
  1. Locking for Stream Refresh/Re-acquisition
  • Introduced a ReentrantLock (passed from BlockInputStream to ChunkInputStream) to guard critical operations such as refreshBlockInfo, acquireClient, and handling read errors. This ensures thread safety during network/client state transitions without locking the entire read path.
  1. Avoid Redundant Pipeline Refreshing
  • Added a failedPipeline variable to track the pipeline that encountered an issue.
  • During refreshBlockInfo, the stream now checks if the current pipeline matches failedPipeline. If it does, the redundant refresh operation is skipped, preventing multiple threads from hammering the refresh logic concurrently for the same failure.
  1. Adopt BlockInputStream Positional Read in OzoneFSInputStream
  • Updated OzoneFSInputStream to leverage the positional read capabilities of BlockInputStream. This allows OzoneFSInputStream to delegate position-based reads directly to the underlying block stream, bypassing global stream-level locks and enabling fully concurrent read operations.

What is the link to the Apache JIRA

https://issues.apache.org/jira/browse/HDDS-15734

How was this patch tested?

Add test in TestChunkInputStream and TestBlockInputStream. Also test by TestOzoneFSInputStream.
CI: https://github.com/chungen0126/ozone/actions/runs/29289303279

@chungen0126

Copy link
Copy Markdown
Contributor Author

cc @yandrey321

@jojochuang

Copy link
Copy Markdown
Contributor

Bugbot review

readFully succeeds at EOFhadoop-hdds/client/src/main/java/org/apache/hadoop/hdds/scm/storage/BlockInputStream.java:328-338 · Severity: high

readFully returns true when the target offset is at or past the end of the block, or when no bytes can be read from the current chunk, instead of throwing EOFException or otherwise signaling failure. Callers such as OzoneFSInputStream treat a true return as a completed positioned read and compute bytes transferred from buffer remaining; an unfilled buffer yields zero bytes read while hasRemaining() stays true, which can spin forever or return silently short reads.

Positioned read leaks clientshadoop-hdds/client/src/main/java/org/apache/hadoop/hdds/scm/storage/ChunkInputStream.java:833-851 · Severity: high

getClientAndUpdateBlock calls acquireClientForReadData on every positioned read(long, ByteBuffer) but never calls releaseClientForReadData. BlockInputStream.readFully uses this path for each chunk segment, so each positioned read leaks an xceiver client and its connections.

readFully checks buffer capacityhadoop-hdds/client/src/main/java/org/apache/hadoop/hdds/scm/storage/ChunkInputStream.java:824-830 · Severity: medium

readFully compares the result of read against byteBuffer.capacity() instead of the number of bytes the caller asked to fill (byteBuffer.remaining() at entry). For a buffer with a non-zero position or a limit below capacity, a successful read can incorrectly throw EOFException, or accept a short read as complete.

Refresh skipped after first failurehadoop-hdds/client/src/main/java/org/apache/hadoop/hdds/scm/storage/BlockInputStream.java:217-223 · Severity: medium

After the first refresh attempt for a pipeline, failedPipeline is set to the current pipelineRef even when refresh did not obtain new location info. Later connectivity failures on the same pipeline skip refreshBlockInfo entirely, so stale pipeline/token metadata may never be refreshed again during retries.


Generated-by: Cursor Bugbot

@peterxcli
peterxcli self-requested a review July 15, 2026 16:41

private BlockData blockData;

private Pipeline failedPipeline;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do we need to track previously failed pipelines?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reason I track previously failed pipelines is that, in my design, I assume a scenario where multiple position read in BlockInputStream might fail concurrently and attempt to refresh the block info at the same time. Once the first BlockInputStream completes the refresh, subsequent streams will obtain the updated information. If the position read detects that its failed pipeline belongs to the old one and a new pipeline is now available, it should not need to trigger another redundant refresh.

int len = buffer.remaining();
int innerRetries = 0;
int chunkIdx = Arrays.binarySearch(chunkOffsets, pos);
if (chunkIdx < 0) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

when index can be negative?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the binary search doesn't find an exact match in the array, it returns a negative number. For details, please see: https://docs.oracle.com/javase/8/docs/api/java/util/Arrays.html#binarySearch-int:A-int-

final List<ChunkInputStream> inputStreams = this.chunkStreams;
if (inputStreams != null) {
for (ChunkInputStream is : inputStreams) {
is.releaseClient();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should it be wrapped in try catch?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it's necessary. They didn't throw any exceptions.

}

@Test
public void testPositionedRead() throws Exception {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these should cover moving forward and back in the file.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have updated the current tests. If you have any suggestions for improvement, please let me know and I'll update them right away.

ByteBuffer byteBuffer = ByteBuffer.wrap(buffer);
int bytesRead = chunkStream.read(30, byteBuffer);

assertEquals(50, bytesRead);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what if chunk stream has less than 50 bytes?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The buffer is filled until the stream reaches its end. For example, if the buffer capacity is 50 but only 10 bytes are left in the stream, only the first 10 bytes of the buffer will be filled.

}

@Test
public void testPositionedReadFully() throws Exception {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how do we assert that stream was read till the end as text name suggest?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As it stands, this method will read as much as possible until either the buffer is full or the stream reaches EOF (End of File). The only way we can verify if we've reached the end of the stream is by checking the final remaining length of the buffer.

However, looking at how the upper-level BlockInputStream actually uses it, we might not even need this method. I think we should consider removing it entirely. What do you think?

Here is how the overall architecture/flow would look:

If the stream reaches EOF but the buffer is not yet full:

  1. ChunkInputStream returns normally to BlockInputStream.

  2. BlockInputStream has two choices:

    a. If there is another ChunkInputStream available, switch to it and continue reading (loop back).
    b. If there are no more chunks, return to MultiPartInputStream.

  3. MultiPartInputStream also has two choices:

    a. If there is another BlockInputStream available, switch to it (loop back).
    b. If there are no more blocks AND the buffer is still not full, throw an EOFException.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants