This is an automated email from the ASF dual-hosted git repository.

smengcl 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 0d57c08f741 HDDS-16620. Expose BlockReadCursor as an iterator of 
streaming ReadBlock ranges (#11442)
0d57c08f741 is described below

commit 0d57c08f74162fe6f92cb1523816dbead0fb7d6d
Author: Peter Lee <[email protected]>
AuthorDate: Thu Oct 8 14:23:45 2026 +0800

    HDDS-16620. Expose BlockReadCursor as an iterator of streaming ReadBlock 
ranges (#11442)
---
 .../ozone/container/keyvalue/BlockReadCursor.java  | 75 ++++++++++++-------
 .../ozone/container/keyvalue/KeyValueHandler.java  | 20 ++---
 .../container/keyvalue/TestBlockReadCursor.java    | 86 ++++++++++++----------
 3 files changed, 107 insertions(+), 74 deletions(-)

diff --git 
a/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/keyvalue/BlockReadCursor.java
 
b/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/keyvalue/BlockReadCursor.java
index e877039829a..3e4567cabf8 100644
--- 
a/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/keyvalue/BlockReadCursor.java
+++ 
b/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/keyvalue/BlockReadCursor.java
@@ -19,19 +19,21 @@
 
 import java.io.IOException;
 import java.util.Arrays;
+import java.util.Iterator;
 import java.util.List;
+import java.util.NoSuchElementException;
 import 
org.apache.hadoop.hdds.protocol.datanode.proto.ContainerProtos.ChecksumType;
 import 
org.apache.hadoop.hdds.protocol.datanode.proto.ContainerProtos.ChunkInfo;
 
-/** Tracks chunk-relative checksum boundaries for a streaming block read. */
-class BlockReadCursor {
+/** Splits a streaming block read into ranges aligned to chunk-relative 
checksum boundaries. */
+class BlockReadCursor implements Iterator<BlockReadCursor.ReadRange> {
   private static final int STREAMING_BYTES_PER_CHUNK = 1024 * 64;
 
   private final List<ChunkInfo> chunks;
   // [c1 offset, c2 offset, ..., cn offset, cn offset + cn len]
   private final long[] chunkOffsets;
+  private final int[] intervals;
   private final int responseDataSize;
-  private final long start;
   private final long end;
   private long offset;
   private int chunkIndex;
@@ -39,27 +41,28 @@ class BlockReadCursor {
   BlockReadCursor(long requestedOffset, long length, int responseSize, 
List<ChunkInfo> chunks) throws IOException {
     this.chunks = chunks;
     chunkOffsets = new long[chunks.size() + 1];
+    intervals = new int[chunks.size()];
     int bufferSize = responseSize;
     for (int i = 0; i < chunks.size(); i++) {
       ChunkInfo chunk = chunks.get(i);
       chunkOffsets[i] = chunk.getOffset();
+      intervals[i] = interval(chunk);
       // One checksum interval (or the short chunk containing it) must always 
fit in the buffer.
-      bufferSize = Math.max(bufferSize, (int) Math.min(chunk.getLen(), 
interval(chunk)));
+      bufferSize = Math.max(bufferSize, (int) Math.min(chunk.getLen(), 
intervals[i]));
     }
     ChunkInfo lastChunk = chunks.get(chunks.size() - 1);
     long blockEnd = lastChunk.getOffset() + lastChunk.getLen();
     chunkOffsets[chunks.size()] = blockEnd;
     this.responseDataSize = bufferSize;
     chunkIndex = findChunk(requestedOffset);
-    start = length == 0 ? requestedOffset : alignDown(requestedOffset, 
chunkIndex);
-    offset = start;
+    offset = length == 0 ? requestedOffset : alignDown(requestedOffset, 
chunkIndex);
     if (length == 0) {
-      end = start;
+      end = offset;
     } else {
       long requestedEnd = requestedOffset + Math.min(length, blockEnd - 
requestedOffset);
       int last = findChunk(requestedEnd - 1);
       long floor = alignDown(requestedEnd - 1, last);
-      end = floor + Math.min(interval(chunks.get(last)), chunkOffsets[last + 
1] - floor);
+      end = floor + Math.min(intervals[last], chunkOffsets[last + 1] - floor);
     }
   }
 
@@ -75,8 +78,8 @@ private static int interval(ChunkInfo chunk) throws 
IOException {
     return size;
   }
 
-  private long alignDown(long position, int index) throws IOException {
-    return position - (position - chunkOffsets[index]) % 
interval(chunks.get(index));
+  private long alignDown(long position, int index) {
+    return position - (position - chunkOffsets[index]) % intervals[index];
   }
 
   private int findChunk(long position) {
@@ -88,11 +91,8 @@ private int findChunk(long position) {
     return insertionPoint - 1;
   }
 
-  long offset() {
-    return offset;
-  }
-
-  boolean hasRemaining() {
+  @Override
+  public boolean hasNext() {
     return offset < end;
   }
 
@@ -100,26 +100,47 @@ int responseDataSize() {
     return responseDataSize;
   }
 
-  int nextReadLength() throws IOException {
+  /** Hands out the next range and advances past it, whether or not the caller 
reads it. */
+  @Override
+  public ReadRange next() {
+    if (!hasNext()) {
+      throw new NoSuchElementException("No range left at offset " + offset + " 
of " + end);
+    }
     long limit = offset + Math.min(responseDataSize, end - offset);
     if (limit < end) {
       limit = alignDown(limit, findChunk(limit));
     }
-    return Math.toIntExact(limit - offset);
+    ReadRange range = new ReadRange(offset, Math.toIntExact(limit - offset),
+        chunks.subList(chunkIndex, findChunk(limit - 1) + 1));
+    offset = limit;
+    if (hasNext()) {
+      chunkIndex = findChunk(offset);
+    }
+    return range;
   }
 
-  List<ChunkInfo> chunksForRead(int length) {
-    return chunks.subList(chunkIndex, findChunk(offset + length - 1) + 1);
-  }
+  /** A range of the block sent in one response, with the chunks it overlaps. 
*/
+  static final class ReadRange {
+    private final long offset;
+    private final int length;
+    private final List<ChunkInfo> chunks;
 
-  void advance(int bytesRead) {
-    offset += bytesRead;
-    if (hasRemaining()) {
-      chunkIndex = findChunk(offset);
+    private ReadRange(long offset, int length, List<ChunkInfo> chunks) {
+      this.offset = offset;
+      this.length = length;
+      this.chunks = chunks;
+    }
+
+    long offset() {
+      return offset;
     }
-  }
 
-  long bytesRead() {
-    return offset - start;
+    int length() {
+      return length;
+    }
+
+    List<ChunkInfo> chunks() {
+      return chunks;
+    }
   }
 }
diff --git 
a/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/keyvalue/KeyValueHandler.java
 
b/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/keyvalue/KeyValueHandler.java
index b4342fcdc91..0ec6965f358 100644
--- 
a/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/keyvalue/KeyValueHandler.java
+++ 
b/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/keyvalue/KeyValueHandler.java
@@ -2348,23 +2348,23 @@ private long readBlockImpl(ContainerCommandRequestProto 
request, RandomAccessFil
     final BlockReadCursor cursor = new BlockReadCursor(readBlock.getOffset(), 
readBlock.getLength(),
         responseDataSize, blockData.getChunks());
     final ByteBuffer buffer = ByteBuffer.allocate(cursor.responseDataSize());
-    blockFile.position(cursor.offset());
-    while (cursor.hasRemaining()) {
-      buffer.clear().limit(cursor.nextReadLength());
+    long bytesRead = 0;
+    while (cursor.hasNext()) {
+      final BlockReadCursor.ReadRange range = cursor.next();
+      blockFile.position(range.offset());
+      buffer.clear().limit(range.length());
       blockFile.read(buffer);
       if (buffer.hasRemaining()) {
-        throw new EOFException("Unexpected end of block " + blockID + " at " + 
cursor.offset());
+        throw new EOFException("Unexpected end of block " + blockID + " at " + 
range.offset());
       }
+      bytesRead += range.length();
       buffer.flip();
-      final int readLength = buffer.remaining();
-      final List<ContainerProtos.ChunkInfo> chunks = 
cursor.chunksForRead(readLength);
       if (validateChunkChecksumData) {
-        Checksum.validateChecksums(buffer, cursor.offset(), 0, chunks);
+        Checksum.validateChecksums(buffer, range.offset(), 0, range.chunks());
       }
-      streamObserver.onNext(getReadBlockResponse(request, chunks, buffer, 
cursor.offset()));
-      cursor.advance(readLength);
+      streamObserver.onNext(getReadBlockResponse(request, range.chunks(), 
buffer, range.offset()));
     }
-    return cursor.bytesRead();
+    return bytesRead;
   }
 
   /**
diff --git 
a/hadoop-hdds/container-service/src/test/java/org/apache/hadoop/ozone/container/keyvalue/TestBlockReadCursor.java
 
b/hadoop-hdds/container-service/src/test/java/org/apache/hadoop/ozone/container/keyvalue/TestBlockReadCursor.java
index 1dda101b8c7..f7360f1a86c 100644
--- 
a/hadoop-hdds/container-service/src/test/java/org/apache/hadoop/ozone/container/keyvalue/TestBlockReadCursor.java
+++ 
b/hadoop-hdds/container-service/src/test/java/org/apache/hadoop/ozone/container/keyvalue/TestBlockReadCursor.java
@@ -19,11 +19,14 @@
 
 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 java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
+import java.util.NoSuchElementException;
 import org.apache.hadoop.hdds.protocol.datanode.proto.ContainerProtos;
 import 
org.apache.hadoop.hdds.protocol.datanode.proto.ContainerProtos.ChunkInfo;
 import org.junit.jupiter.api.Test;
@@ -36,36 +39,51 @@ private static ChunkInfo chunk(long offset, long length, 
int interval) {
             
.setType(ContainerProtos.ChecksumType.CRC32).setBytesPerChecksum(interval)).build();
   }
 
+  private static void assertNextRange(BlockReadCursor cursor, long offset, int 
length, List<ChunkInfo> chunks) {
+    assertTrue(cursor.hasNext());
+    BlockReadCursor.ReadRange range = cursor.next();
+    assertEquals(offset, range.offset());
+    assertEquals(length, range.length());
+    assertEquals(chunks, range.chunks());
+  }
+
   @Test
   void testShiftedRangesAndLookahead() throws Exception {
     List<ChunkInfo> chunks = Arrays.asList(chunk(0, 3, 4), chunk(3, 9, 4), 
chunk(12, 7, 3));
     BlockReadCursor cursor = new BlockReadCursor(4, 14, 6, chunks);
-    assertEquals(3, cursor.offset());
-    assertEquals(4, cursor.nextReadLength());
-    assertEquals(4, cursor.nextReadLength());
-    assertEquals(chunks.subList(1, 2), cursor.chunksForRead(4));
-    cursor.advance(4);
-    assertEquals(7, cursor.offset());
-    assertEquals(5, cursor.nextReadLength());
-    cursor.advance(5);
-    assertEquals(12, cursor.offset());
-    assertEquals(6, cursor.nextReadLength());
-    cursor.advance(6);
-    assertFalse(cursor.hasRemaining());
-    assertEquals(15, cursor.bytesRead());
+    assertTrue(cursor.hasNext());
+    assertNextRange(cursor, 3, 4, chunks.subList(1, 2));
+    assertNextRange(cursor, 7, 5, chunks.subList(1, 2));
+    assertNextRange(cursor, 12, 6, chunks.subList(2, 3));
+    assertFalse(cursor.hasNext());
   }
 
   @Test
   void testSpanningChunksAndShortFinalChecksum() throws Exception {
     List<ChunkInfo> chunks = Arrays.asList(chunk(0, 3, 4), chunk(3, 9, 4));
     BlockReadCursor cursor = new BlockReadCursor(0, Long.MAX_VALUE, 8, chunks);
-    assertEquals(7, cursor.nextReadLength());
-    assertEquals(chunks, cursor.chunksForRead(7));
-    cursor.advance(7);
-    assertEquals(5, cursor.nextReadLength());
-    cursor.advance(5);
-    assertFalse(cursor.hasRemaining());
-    assertEquals(12, cursor.bytesRead());
+    assertNextRange(cursor, 0, 7, chunks);
+    assertNextRange(cursor, 7, 5, chunks.subList(1, 2));
+    assertFalse(cursor.hasNext());
+  }
+
+  @Test
+  void testIteratesWholeRangeOnce() throws Exception {
+    List<ChunkInfo> chunks = Arrays.asList(chunk(0, 3, 4), chunk(3, 9, 4), 
chunk(12, 7, 3));
+    BlockReadCursor cursor = new BlockReadCursor(0, Long.MAX_VALUE, 5, chunks);
+    List<Integer> lengths = new ArrayList<>();
+    long offset = 0;
+    while (cursor.hasNext()) {
+      BlockReadCursor.ReadRange range = cursor.next();
+      assertEquals(offset, range.offset());
+      lengths.add(range.length());
+      offset += range.length();
+    }
+    assertEquals(Arrays.asList(3, 4, 5, 3, 4), lengths);
+    assertFalse(cursor.hasNext());
+    assertThrows(NoSuchElementException.class, cursor::next);
+    assertFalse(cursor.hasNext());
+    assertThrows(NoSuchElementException.class, new BlockReadCursor(3, 0, 1, 
chunks)::next);
   }
 
   @Test
@@ -75,12 +93,8 @@ void testReadsAtChunkAndChecksumBoundaries() throws 
Exception {
         {12, 15, 2}, {15, 18, 2}, {18, 19, 2}}) {
       for (int requestedOffset : new int[] {range[0], range[1] - 1}) {
         BlockReadCursor cursor = new BlockReadCursor(requestedOffset, 1, 8, 
chunks);
-        int length = range[1] - range[0];
-        assertEquals(range[0], cursor.offset());
-        assertEquals(length, cursor.nextReadLength());
-        assertEquals(Collections.singletonList(chunks.get(range[2])), 
cursor.chunksForRead(length));
-        cursor.advance(length);
-        assertFalse(cursor.hasRemaining());
+        assertNextRange(cursor, range[0], range[1] - range[0], 
Collections.singletonList(chunks.get(range[2])));
+        assertFalse(cursor.hasNext());
       }
     }
   }
@@ -90,29 +104,27 @@ void testMinimumBufferAndExactChunkEnd() throws Exception {
     List<ChunkInfo> chunks = Arrays.asList(chunk(0, 3, 4), chunk(3, 9, 4));
     BlockReadCursor cursor = new BlockReadCursor(3, 9, 1, chunks);
     for (int expected : new int[] {4, 4, 1}) {
-      assertTrue(cursor.hasRemaining());
-      assertEquals(expected, cursor.nextReadLength());
-      cursor.advance(expected);
+      assertTrue(cursor.hasNext());
+      assertEquals(expected, cursor.next().length());
     }
-    assertFalse(cursor.hasRemaining());
-    assertFalse(new BlockReadCursor(3, 0, 1, chunks).hasRemaining());
+    assertFalse(cursor.hasNext());
+    assertFalse(new BlockReadCursor(3, 0, 1, chunks).hasNext());
     BlockReadCursor shortChunk = new BlockReadCursor(0, 3, 1,
         Collections.singletonList(chunk(0, 3, Integer.MAX_VALUE)));
     assertEquals(3, shortChunk.responseDataSize());
-    assertEquals(3, shortChunk.nextReadLength());
+    assertEquals(3, shortChunk.next().length());
   }
 
   @Test
   void testRangeNearLongLimit() throws Exception {
     List<ChunkInfo> chunks = Arrays.asList(chunk(0, Long.MAX_VALUE - 9, 4), 
chunk(Long.MAX_VALUE - 9, 9, 4));
     BlockReadCursor cursor = new BlockReadCursor(Long.MAX_VALUE - 8, 
Long.MAX_VALUE, 4, chunks);
-    assertEquals(Long.MAX_VALUE - 9, cursor.offset());
+    long offset = Long.MAX_VALUE - 9;
     for (int length : new int[] {4, 4, 1}) {
-      assertEquals(length, cursor.nextReadLength());
-      cursor.advance(length);
+      assertNextRange(cursor, offset, length, chunks.subList(1, 2));
+      offset += length;
     }
-    assertEquals(Long.MAX_VALUE, cursor.offset());
-    assertFalse(cursor.hasRemaining());
+    assertFalse(cursor.hasNext());
   }
 
 }


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to