Skip to content

HDDS-15734. Implementing position read in BlockInputStream#10765

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

HDDS-15734. Implementing position read in BlockInputStream#10765
chungen0126 wants to merge 7 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.

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?

}

@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?

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