JingsongLi commented on code in PR #10723:
URL: https://github.com/apache/paimon/pull/10723#discussion_r4345134243


##########
paimon-vortex/paimon-vortex-format/src/main/java/org/apache/paimon/format/vortex/VortexRecordsReader.java:
##########
@@ -21,195 +21,425 @@
 import org.apache.paimon.annotation.VisibleForTesting;
 import org.apache.paimon.arrow.reader.ArrowBatchReader;
 import org.apache.paimon.data.InternalRow;
+import org.apache.paimon.data.columnar.AllNullColumnVector;
+import org.apache.paimon.data.columnar.ColumnVector;
+import org.apache.paimon.data.columnar.ColumnarRow;
+import org.apache.paimon.data.columnar.VectorizedColumnBatch;
+import org.apache.paimon.data.columnar.VectorizedRowIterator;
 import org.apache.paimon.fs.Path;
 import org.apache.paimon.reader.FileRecordIterator;
 import org.apache.paimon.reader.FileRecordReader;
 import org.apache.paimon.table.SpecialFields;
 import org.apache.paimon.types.DataField;
 import org.apache.paimon.types.RowType;
+import org.apache.paimon.utils.ExecutorThreadFactory;
 import org.apache.paimon.utils.LongIterator;
-import org.apache.paimon.utils.ProjectedRow;
+import org.apache.paimon.utils.RoaringBitmap32;
 
 import dev.vortex.api.DataSource;
 import dev.vortex.api.Expression;
 import dev.vortex.api.ImmutableScanOptions;
 import dev.vortex.api.Partition;
 import dev.vortex.api.Scan;
 import dev.vortex.api.ScanOptions;
-import dev.vortex.api.Session;
 import dev.vortex.arrow.ArrowAllocation;
+import dev.vortex.arrow.ArrowBatch;
+import dev.vortex.arrow.ArrowBatchStream;
+import dev.vortex.arrow.ViewColumn;
 import org.apache.arrow.memory.BufferAllocator;
+import org.apache.arrow.vector.UInt8Vector;
 import org.apache.arrow.vector.VectorSchemaRoot;
-import org.apache.arrow.vector.ipc.ArrowReader;
 
 import javax.annotation.Nullable;
 
+import java.io.Closeable;
 import java.io.IOException;
+import java.io.InterruptedIOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
 import java.util.ArrayList;
 import java.util.HashSet;
-import java.util.Iterator;
 import java.util.List;
-import java.util.Map;
 import java.util.Set;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
 
-/** File reader for Vortex format. */
+/**
+ * File reader for Vortex format.
+ *
+ * <p>Each Arrow batch from the native scan is a new root that this reader 
owns until the consumer
+ * releases the batch, so a later batch never overwrites one that the consumer 
still holds. Batches
+ * are returned as {@link VectorizedRowIterator}s, so engines can consume them 
column by column.
+ *
+ * <p>A pushed-down filter or a row selection drops rows in native code. The 
file position of each
+ * returned row then comes from the Vortex row index, because deletion vectors 
and row tracking
+ * depend on it.
+ */
 public class VortexRecordsReader implements FileRecordReader<InternalRow> {
 
-    private final ArrowBatchReader arrowBatchReader;
+    private static final String ROW_INDEX_FIELD = "_PAIMON_VORTEX_ROW_INDEX";
+
     private final Path filePath;
     private final BufferAllocator allocator;
-    private final Session session;
+    @Nullable private final Closeable readable;
     private final DataSource dataSource;
     private final Scan scan;
-    private final LongIterator positionIterator;
+    private final ArrowBatchReader arrowBatchReader;
     @Nullable private final int[] physicalFieldMapping;
-    private ArrowReader currentArrowReader;
-    private Partition currentPartition;
-    private long returnedPosition = -1;
-
-    public VortexRecordsReader(
-            Path path,
+    private final boolean readsRowIndex;
+    private final Set<ArrowBatch> activeBatches = new HashSet<>();
+
+    /** Fetches the next batch while the consumer processes the current one. */
+    private static final ExecutorService PREFETCH_POOL =
+            Executors.newCachedThreadPool(new 
ExecutorThreadFactory("vortex-batch-prefetch"));
+
+    // Only one thread at a time calls fetchNext: the caller, or the prefetch 
it waits for.
+    @Nullable private ArrowBatchStream currentStream;
+    @Nullable private Future<ArrowBatch> prefetched;
+    private long nextPosition;
+    private boolean closed;
+
+    VortexRecordsReader(
+            VortexInput input,
+            Path filePath,
             RowType dataSchemaRowType,
             RowType projectedRowType,
-            @Nullable long[] rowIndices,
-            @Nullable Expression predicate,
-            Map<String, String> storageOptions) {
-        this.filePath = path;
+            @Nullable RoaringBitmap32 selection,
+            @Nullable Expression filter) {
+        this.filePath = filePath;
+
         RowType physicalReadRowType = physicalReadRowType(dataSchemaRowType, 
projectedRowType);
         this.physicalFieldMapping =
                 physicalFieldMapping(dataSchemaRowType, physicalReadRowType, 
projectedRowType);
+        this.arrowBatchReader =
+                new ArrowBatchReader(physicalReadRowType, true, 
VortexArrowConverters.INSTANCE);
+
+        List<String> columns = physicalReadRowType.getFieldNames();
+        this.readsRowIndex = filter != null || selection != null || 
columns.isEmpty();
+
         this.allocator =
                 ArrowAllocation.rootAllocator()
                         .newChildAllocator("vortex-reader", 0, Long.MAX_VALUE);
 
+        Closeable openedReadable = null;
+        DataSource openedDataSource = null;
         try {
-            this.session = Session.create();
-            try {
-                this.dataSource = DataSource.open(session, 
path.toUri().toString(), storageOptions);
-                try {
-                    ImmutableScanOptions.Builder scanBuilder =
-                            ImmutableScanOptions.builder().ordered(true);
-
-                    java.util.List<String> columns = 
physicalReadRowType.getFieldNames();
-                    // The scan clones the projection it is given, so both the 
root and the select
-                    // built here are ours to release once dataSource.scan has 
returned.
-                    try (Expression root = Expression.root();
-                            Expression projection =
-                                    Expression.select(columns.toArray(new 
String[0]), root)) {
-                        scanBuilder.projection(projection);
-
-                        if (rowIndices != null) {
-                            scanBuilder.selectionIndices(rowIndices);
-                            
scanBuilder.selectionMode(ScanOptions.SelectionMode.INCLUDE);
-                        }
-                        if (predicate != null) {
-                            scanBuilder.filter(predicate);
-                        }
-                        this.scan = dataSource.scan(scanBuilder.build());
-                    }
-                } catch (Exception e) {
-                    dataSource.close();
-                    throw e;
-                }
-            } catch (Exception e) {
-                session.close();
-                throw e;
+            openedReadable = input.readable();
+            openedDataSource = input.open();
+            // The scan clones the expressions it is given, so the projection 
is ours to release
+            // once the scan exists.
+            try (Expression projection = projection(columns)) {
+                this.scan = openedDataSource.scan(scanOptions(projection, 
selection, filter));
             }
-        } catch (Exception e) {
-            allocator.close();
-            throw new RuntimeException(e);
+        } catch (Throwable t) {
+            closeQuietly(openedDataSource, t);
+            closeQuietly(openedReadable, t);
+            closeQuietly(allocator, t);
+            throw t;
         }
 
-        this.arrowBatchReader = new ArrowBatchReader(physicalReadRowType, 
true);
-        long totalRowCount = dataSource.rowCount();
-        this.positionIterator =
-                rowIndices != null
-                        ? LongIterator.fromArray(rowIndices)
-                        : LongIterator.fromRange(0, totalRowCount > 0 ? 
totalRowCount : 0);
+        this.readable = openedReadable;
+        this.dataSource = openedDataSource;
+
+        // Start decoding the first batch now. A merge opens all files of a 
split before reading
+        // any of them, so their first batches then decode in parallel.
+        this.prefetched = PREFETCH_POOL.submit(this::fetchNext);
     }
 
     @Nullable
     @Override
     public FileRecordIterator<InternalRow> readBatch() throws IOException {
-        // Try to load the next batch from the current ArrowReader
-        if (currentArrowReader != null && currentArrowReader.loadNextBatch()) {
-            return toBatchIterator(currentArrowReader.getVectorSchemaRoot());
+        while (true) {
+            ArrowBatch batch = takeNext();
+            if (batch == null) {
+                return null;
+            }
+
+            track(batch);
+            if (batch.rowCount() > 0) {
+                prefetched = PREFETCH_POOL.submit(this::fetchNext);
+                return toBatchIterator(batch);
+            }
+            releaseBatch(batch);
         }
+    }
 
-        // Close current reader and move to the next partition
-        closeCurrentArrowReader();
+    @Nullable
+    private ArrowBatch takeNext() throws IOException {
+        if (prefetched == null) {
+            return fetchNext();
+        }
+
+        Future<ArrowBatch> future = prefetched;
+        prefetched = null;

Review Comment:
   [P1] Retain and drain the in-flight prefetch before releasing native 
resources
   
   If the reading thread is interrupted while waiting on a slow prefetch, 
`await` throws InterruptedIOException with the interrupt flag restored, but 
this assignment has already detached the still-running future. A subsequent 
`close()` therefore skips waiting and closes the Arrow stream, scan, readable 
and allocator while `fetchNext()` may still be inside native getNext/IO. 
Directly interrupting close has the same problem: its interruptible `await` 
exits and cleanup continues. A latch-controlled harness against these branch 
classes demonstrates `readableClosedWhilePrefetchRunning=true` in both paths. 
Keep the future reachable until actual completion and drain it uninterruptibly 
during cleanup, restoring interruption afterward; merely cancelling the Future 
is insufficient if native IO continues running.



-- 
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]

Reply via email to