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

JingsongLi pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/paimon.git


The following commit(s) were added to refs/heads/master by this push:
     new 8548906653 [mosaic] Respect decoded byte budget when refilling prefetch
8548906653 is described below

commit 85489066537858239156d295e9737bb0be459a32
Author: JingsongLi <[email protected]>
AuthorDate: Fri Sep 11 22:10:54 2026 +0800

    [mosaic] Respect decoded byte budget when refilling prefetch
---
 .../paimon/format/mosaic/MosaicRecordsReader.java  | 10 ++--
 .../format/mosaic/MosaicRecordsReaderTest.java     | 69 ++++++++++++----------
 2 files changed, 44 insertions(+), 35 deletions(-)

diff --git 
a/paimon-mosaic/src/main/java/org/apache/paimon/format/mosaic/MosaicRecordsReader.java
 
b/paimon-mosaic/src/main/java/org/apache/paimon/format/mosaic/MosaicRecordsReader.java
index 6168418097..e9a1712b77 100644
--- 
a/paimon-mosaic/src/main/java/org/apache/paimon/format/mosaic/MosaicRecordsReader.java
+++ 
b/paimon-mosaic/src/main/java/org/apache/paimon/format/mosaic/MosaicRecordsReader.java
@@ -294,7 +294,7 @@ public class MosaicRecordsReader implements 
FileRecordReader<InternalRow> {
     @Nullable
     private RowGroupBatch nextRowGroup() throws IOException {
         if (pending.isEmpty()) {
-            fillPrefetchQueue(Math.max(1, prefetchDepth));
+            fillPrefetchQueue(Math.max(1, prefetchDepth), true);
         }
         RowGroupBatch head = pending.peek();
         if (head == null) {
@@ -310,13 +310,13 @@ public class MosaicRecordsReader implements 
FileRecordReader<InternalRow> {
         currentVsr = vsr;
         if (prefetchDepth > 0) {
             // currentVsr is owned by this reader, so a failure here leaves 
nothing unreleased.
-            fillPrefetchQueue(prefetchDepth);
+            fillPrefetchQueue(prefetchDepth, false);
         }
         return head;
     }
 
     /** Schedules matching row groups until {@code wanted} are queued or the 
byte budget is used. */
-    private void fillPrefetchQueue(int wanted) {
+    private void fillPrefetchQueue(int wanted, boolean readOnDemand) {
         while (pending.size() < wanted && nextRowGroupToSchedule < 
numRowGroups) {
             int index = nextRowGroupToSchedule;
             int numRows = reader.rowGroupNumRows(index);
@@ -327,8 +327,8 @@ public class MosaicRecordsReader implements 
FileRecordReader<InternalRow> {
                 continue;
             }
             long bytes = numRows * estimatedRowBytes;
-            // The first queued row group is always read; the rest must fit 
the decoded budget.
-            if (!pending.isEmpty() && pendingBytes + bytes > prefetchMaxBytes) 
{
+            // Only an on-demand read may exceed the decoded budget to make 
progress.
+            if ((!readOnDemand || !pending.isEmpty()) && pendingBytes + bytes 
> prefetchMaxBytes) {
                 return;
             }
             nextRowGroupToSchedule++;
diff --git 
a/paimon-mosaic/src/test/java/org/apache/paimon/format/mosaic/MosaicRecordsReaderTest.java
 
b/paimon-mosaic/src/test/java/org/apache/paimon/format/mosaic/MosaicRecordsReaderTest.java
index 0e88b42909..ca5af5f7e4 100644
--- 
a/paimon-mosaic/src/test/java/org/apache/paimon/format/mosaic/MosaicRecordsReaderTest.java
+++ 
b/paimon-mosaic/src/test/java/org/apache/paimon/format/mosaic/MosaicRecordsReaderTest.java
@@ -36,6 +36,8 @@ import org.apache.arrow.vector.types.pojo.ArrowType;
 import org.apache.arrow.vector.types.pojo.Field;
 import org.apache.arrow.vector.types.pojo.Schema;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
 
 import java.io.IOException;
 import java.io.InterruptedIOException;
@@ -55,7 +57,6 @@ import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.doThrow;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.timeout;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
@@ -369,39 +370,47 @@ class MosaicRecordsReaderTest {
         assertThat(allocator.closeCount()).isEqualTo(1);
     }
 
-    @Test
-    void testPrefetchIsBoundedByEstimatedDecodedBytes() throws IOException {
+    @ParameterizedTest
+    @CsvSource({
+        "0, 1, 1",
+        "4000, 1, 1",
+        "4000, 4, 4",
+        "5000, 1, 2",
+        "9999, 1, 2",
+        "10000, 1, 3",
+        "100000, 1, 4"
+    })
+    void testPrefetchIsBoundedByEstimatedDecodedBytes(
+            long budget, int batchesToRead, int expectedReads) throws 
IOException {
         // One INT column: 5 bytes per row; 1,000 rows per row group is 5,000 
bytes.
         
assertThat(MosaicRecordsReader.estimatedRowBytes(rowType())).isEqualTo(5);
-        for (long budget : new long[] {4_000L, 100_000L}) {
-            CloseCountingSeekableInputStream inputStream = new 
CloseCountingSeekableInputStream();
-            MosaicInputFileAdapter inputFileAdapter = 
createInputFileAdapter(inputStream);
-            CloseCountingRootAllocator allocator = new 
CloseCountingRootAllocator();
-            MosaicReader reader = createProjectedReader(allocator, 4);
-            when(reader.rowGroupNumRows(anyInt())).thenReturn(1000);
-            MosaicRecordsReader recordsReader =
-                    new MosaicRecordsReader(
-                            inputFileAdapter,
-                            0,
-                            rowType(),
-                            rowType(),
-                            null,
-                            new Path("file:/tmp/mosaic-reader-test"),
-                            allocator,
-                            (inputFile, fileSize, bufferAllocator) -> reader,
-                            8,
-                            budget);
-            assertThat(recordsReader.readBatch()).isNotNull();
-            if (budget < 5_000L) {
-                // Below one row group: nothing is read ahead of the batch 
being consumed.
-                verify(reader, times(1)).readRowGroup(anyInt(), any());
-            } else {
-                // The three remaining row groups fit the budget and are read 
ahead.
-                verify(reader, timeout(5_000).times(4)).readRowGroup(anyInt(), 
any());
+        CloseCountingSeekableInputStream inputStream = new 
CloseCountingSeekableInputStream();
+        MosaicInputFileAdapter inputFileAdapter = 
createInputFileAdapter(inputStream);
+        CloseCountingRootAllocator allocator = new 
CloseCountingRootAllocator();
+        MosaicReader reader = createProjectedReader(allocator, 4);
+        when(reader.rowGroupNumRows(anyInt())).thenReturn(1000);
+        try (MosaicRecordsReader recordsReader =
+                new MosaicRecordsReader(
+                        inputFileAdapter,
+                        0,
+                        rowType(),
+                        rowType(),
+                        null,
+                        new Path("file:/tmp/mosaic-reader-test"),
+                        allocator,
+                        (inputFile, fileSize, bufferAllocator) -> reader,
+                        8,
+                        budget)) {
+            for (int i = 0; i < batchesToRead; i++) {
+                FileRecordIterator<InternalRow> batch = 
recordsReader.readBatch();
+                assertThat(batch).isNotNull();
+                assertThat(batch.next().getInt(0)).isEqualTo(i);
+                batch.releaseBatch();
             }
-            recordsReader.close();
-            assertThat(allocator.closeCount()).isEqualTo(1);
         }
+        // Closing drains scheduled reads, so verification cannot race with 
background tasks.
+        verify(reader, times(expectedReads)).readRowGroup(anyInt(), any());
+        assertThat(allocator.closeCount()).isEqualTo(1);
     }
 
     private static void closeQuietly(

Reply via email to