dongjoon-hyun commented on code in PR #59934:
URL: https://github.com/apache/spark/pull/59934#discussion_r4471627269


##########
sql/core/src/main/resources/org/apache/spark/sql/datasource/spark_datasource.h:
##########
@@ -304,29 +313,135 @@ typedef struct SparkDsCreateInfo {
   int64_t num_options;
 } SparkDsCreateInfo;
 
+/* A batch scan, as create_reader gets it. */
+typedef struct SparkDsReaderInfo {
+  /* The size of this struct in the header that Spark was built with, as in 
SparkDsHost. */
+  int64_t size;
+} SparkDsReaderInfo;
+
+/* A request for the partitions of a batch scan, as partitions gets it. */
+typedef struct SparkDsPartitionsInfo {
+  /* The size of this struct in the header that Spark was built with, as in 
SparkDsHost. */
+  int64_t size;
+} SparkDsPartitionsInfo;
+
+/* A micro-batch streaming scan, as create_stream_reader gets it. */
+typedef struct SparkDsStreamReaderInfo {
+  /* The size of this struct in the header that Spark was built with, as in 
SparkDsHost. */
+  int64_t size;
+  /*
+   * The location of a directory in the checkpoint of the streaming query that 
is reserved for this
+   * source, such as file:/tmp/checkpoint/sources/0, where the library may 
keep what it needs to
+   * recover from a failure. It stays the same when the query restarts from 
its checkpoint. It is a
+   * fully qualified Hadoop path, whose characters are not escaped as in a 
URI: a space stays a
+   * space, and % stays %. Its file system may be one that the library cannot 
access, such as s3a
+   * or hdfs. Spark keeps what it needs there under the names that start with 
_spark, such as the
+   * initial offset of the source, and the library must not use them.
+   */
+  const char* checkpoint_location;
+} SparkDsStreamReaderInfo;
+
+/* A request for the initial offset of a stream reader, as initial_offset gets 
it. */
+typedef struct SparkDsInitialOffsetInfo {
+  /* The size of this struct in the header that Spark was built with, as in 
SparkDsHost. */
+  int64_t size;
+} SparkDsInitialOffsetInfo;
+
+/* A request for the latest offset of a stream reader, as latest_offset gets 
it. */
+typedef struct SparkDsLatestOffsetInfo {
+  /* The size of this struct in the header that Spark was built with, as in 
SparkDsHost. */
+  int64_t size;
+  /*
+   * The offset, as JSON, that the next micro-batch starts after: the end of 
the last micro-batch,
+   * or the initial offset before the first one, for every trigger. With 
Trigger.AvailableNow,
+   * Spark asks for the latest offset only once, before the first micro-batch 
that it plans, and
+   * reads up to it.
+   */
+  const char* start;
+} SparkDsLatestOffsetInfo;
+
+/* A request for the partitions of a micro-batch, as stream_partitions gets 
it. */
+typedef struct SparkDsStreamPartitionsInfo {
+  /* The size of this struct in the header that Spark was built with, as in 
SparkDsHost. */
+  int64_t size;
+  /* The offsets, as JSON, that the micro-batch reads the data after, up to 
and including end. */
+  const char* start;
+  const char* end;
+} SparkDsStreamPartitionsInfo;
+
+/* The end of the data that Spark processed, as commit_offset gets it. */
+typedef struct SparkDsCommitOffsetInfo {
+  /* The size of this struct in the header that Spark was built with, as in 
SparkDsHost. */
+  int64_t size;
+  /* The offset, as JSON, up to and including which Spark processed the data. 
*/
+  const char* end;
+} SparkDsCommitOffsetInfo;
+
 /*
  * The task that calls read or create_data_writer, on an executor. It stays 
valid until the stream
- * that read returns is released, or until the data writer is closed.
+ * that read returns is released, or until the data writer is closed. The 
stream of a read may be
+ * released once the task ended, from another thread: an engine that runs a 
query in native code
+ * may get the stream from Spark, read it in the task that opened it and 
release it later, and
+ * Spark itself may read a stream on a thread that the task does not wait for, 
for example the one
+ * of a PipedRDD. The task stays valid until then too, and for a read, 
is_interrupted then also
+ * returns true once the task failed or completed, so the library may stop 
reading then. Such an
+ * engine may also keep the arrays and the schemas of the stream after it 
released the stream, so
+ * the release of an array or a schema must not use the stream or the task.
  */
 typedef struct SparkDsTaskContext {
   /* The size of this struct in the header that Spark was built with, as in 
SparkDsHost. */
   int64_t size;
   /* The ID of the stage of the task, and the attempt number of the stage. */
   int32_t stage_id;
   int32_t stage_attempt_number;
-  /* The partition that the task reads or writes, and the attempt number of 
the task. */
+  /*
+   * The partition: for read, the index of the partition in the list that 
partitions or
+   * stream_partitions returned, which is not the partition of the task when 
Spark reads several
+   * scans or partitions in one task, for example after a union or a coalesce. 
It is unique within
+   * a scan, but a task that reads several scans may get the same index for 
each. For
+   * create_data_writer, the partition of the task that writes. Then the 
attempt number of the
+   * task.
+   */
   int32_t partition_id;

Review Comment:
   **2. [Low] Could the index of the partition in the scan be a field of 
`SparkDsReadInfo`?**
   
   Since b98fd19, which answered [my item 1 of round 
3](https://github.com/apache/spark/pull/59934#discussion_r4418419351), 
`partition_id` is the index of the partition in the scan for `read`, and the 
partition of the task for `create_data_writer`. This comment documents both, 
and that a task that reads several scans may get the same index for each. That 
leaves a read with no way to get the partition of its task, and gives the same 
field of the same struct two meanings, while `SparkDsReadInfo` already 
describes the partition being read (`kind`, `partition`).
   
   Since #59931 settles version 1 of the interface, could `partition_id` stay 
the partition of the task for both, and `SparkDsReadInfo` get the index, for 
example as `partition_index` after `partition_length`? Then `(stage_id, 
stage_attempt_number, partition_id, attempt_number)` identifies the task in a 
read as it does in a data writer, and the index stays unique within a scan, as 
now. If you prefer the current shape, since `task_attempt_id` already gives a 
unique key, that is fine with me too.



##########
sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/v2/columnar/ColumnarTable.scala:
##########
@@ -178,30 +204,89 @@ class ColumnarScan(
  * The batch of a scan, which plans it once, when Spark first asks for its 
partitions or its reader
  * factory. A failure is planned once too: the physical plan can ask again, 
for example from a lazy
  * value, and gets the same error.
+ *
+ * @param projection the ordinals of the columns that the scan reads, if the 
partitions return
+ *                   more columns
  */
-class ColumnarBatchScan(planner: ColumnarBatchPlanner) extends Batch {
+class ColumnarBatchScan(planner: ColumnarBatchPlanner, projection: 
Option[Array[Int]] = None)
+  extends Batch {
   private lazy val planned: Try[ColumnarPlannedBatch] = Try(planner.plan())
 
   override def planInputPartitions(): Array[InputPartition] = 
planned.get.partitions
 
-  override def createReaderFactory(): PartitionReaderFactory = 
planned.get.readerFactory
+  override def createReaderFactory(): PartitionReaderFactory = {

Review Comment:
   **1. [Medium] A batch scan whose library does not prune the columns loses 
`SupportsArrowStreamRead`**
   
   Since 870e3fa, which came from [my review of 
#59943](https://github.com/apache/spark/pull/59943#discussion_r4463353297), a 
batch scan that reads fewer columns than the full schema wraps the factory of 
the planner in a `ColumnarProjectionReaderFactory` when the planner does not 
prune them. That wrapper does not implement `SupportsArrowStreamRead`. So for a 
library that leaves `prune_columns` NULL, or returns `pruned = false`, a query 
such as `select("id")` gives `BatchScanExec.readerFactory` a factory without 
the interface.
   
   The Javadoc (`SupportsArrowStreamRead.java` L27-L29), the guide 
(`sql-native-data-sources.md` L254-L255) and the PR description say that the 
reader factories of native batch scans implement it. An engine that casts, as 
the test does at `NativeDataSourceSuite.scala` L1549, then gets a 
`ClassCastException`, and one that checks falls back to Spark's import without 
telling why. The test library always prunes (`test_native_datasource.cc` 
L1188), so no test reaches this case.
   
   A wrapper that only delegated `openArrowStream` would return a stream with 
all the columns of the partitions, not those of the read schema that the 
Javadoc promises (L31-L32). So could the docs say that the factory implements 
it only when the library pruned the columns, or could the wrapper implement it 
and project the stream too, as it projects the batches? Either way, a test with 
a library that does not prune would cover it.



##########
sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/v2/ffi/NativeDataSource.scala:
##########
@@ -886,10 +998,135 @@ class NativePartitionReader(
   }
 }
 
-/** Writes the batches of a task to the data writer of a native library. */
+object NativePartitionReader {
+  import NativeCalls._
+
+  /**
+   * Initializes the stream at `streamAddress` with the data of a partition 
for an engine, see
+   * [[SupportsArrowStreamRead]]: the context of its task is closed once the 
stream is released.
+   * It first checks the arguments of `openArrowStream`, before it looks at 
the partition, which
+   * must be a `P`, the class of the partitions of the scan of the reader 
factory. It then reads
+   * the partition with the state and the partition of the library that 
`stateAndPartition`
+   * returns for it.
+   */
+  private[ffi] def openEngineStream[P <: InputPartition: ClassTag](
+      location: NativeLibraryLocation,
+      name: String,
+      kind: Int,
+      partition: InputPartition,
+      schema: StructType,
+      timeZoneId: String,
+      streamAddress: Long)(
+      stateAndPartition: P => (Array[Byte], NativeInputPartition)): Unit = {
+    val context = TaskContext.get()
+    if (context != null) {
+      // A task that was killed gets the error of its kill, as Spark reports 
it.
+      context.killTaskIfInterrupted()
+    }
+    // A task that failed is over too, also while its listeners run.
+    if (context == null || context.isFailed() || context.isCompleted()) {
+      throw new IllegalStateException("openArrowStream must be called in the 
running task that " +
+        "reads the partition, on a thread that has its TaskContext.")
+    }
+    if (streamAddress == 0 || 
ArrowArrayStream.wrap(streamAddress).snapshot().release != 0) {
+      throw new IllegalArgumentException(
+        "openArrowStream needs the address of a stream whose release callback 
is NULL.")
+    }
+    val (readerState, nativePartition) = partition match {
+      case expected: P => stateAndPartition(expected)
+      case _ =>
+        throw new IllegalArgumentException("openArrowStream needs a partition 
of the scan of " +
+          s"the reader factory, a ${classTag[P].runtimeClass.getSimpleName}, 
but got " +
+          s"${Option(partition).map(_.getClass.getName).orNull}.")
+    }
+    val task = openStream(location, name, kind, readerState, nativePartition, 
schema, timeZoneId,
+      streamAddress)
+    try {
+      NativeLibrary.wrapEngineStream(streamAddress, task, name)

Review Comment:
   **3. [Nit] A failure to create the callbacks is not a 
`NATIVE_DATA_SOURCE_ERROR`**
   
   `call` rethrows the link errors as `NATIVE_DATA_SOURCE_ERROR` too, "such as 
when Arrow cannot load its own JNI library" (L1312-L1327). This 
`wrapEngineStream` runs outside of a `call`, though. So if 
`EngineStreamCallbacks` cannot be created, the engine gets the 
`ExceptionInInitializerError` of its first use, and a `NoClassDefFoundError` 
from each later one, rather than an error that names the data source.
   
   The stream is still released by `releaseOnFailure`, so only the error 
differs from the one of a failing `read`, which the test checks. Could this 
line run in `call(name, "read")`, as `library.read` does in `openStream`?



##########
sql/core/src/test/scala/org/apache/spark/sql/execution/datasources/v2/ffi/NativeDataSourceSuite.scala:
##########
@@ -1180,6 +1964,7 @@ class NativeDataSourceSuite extends QueryTest with 
SharedSparkSession {
     val marker = new File(Utils.createTempDir(), "released")
     val reading = marker.getPath + ".reading"
     val df = range(session, "end" -> "10", "partitions" -> "1", "slow_read" -> 
marker.getPath)
+    val contexts = NativeLibrary.openTaskContexts()

Review Comment:
   **4. [Nit] The killed task of the previous test may close its context after 
`contexts` is read**
   
   "tell a read that its task was killed" ends once the library wrote its 
marker (L1956-L1958), but its task only closes the context after `read` 
returned and Spark closed the reader, which may happen after this line. 
`contexts` would then count that context, and `== contexts + 1` at L1986 would 
fail, since the count went down by one meanwhile. Reading the count right 
before each step, which answered [my item 5 of round 
4](https://github.com/apache/spark/pull/59934#discussion_r4419050277), narrows 
the window but does not close it, and `checkContexts` (L1577-L1585) would have 
the same exposure if a test that kills a task ran before it.
   
   Could the test of the kill also wait for its context, as the kill part of 
the stream test does at L1702? For example, it could read 
`NativeLibrary.openTaskContexts()` before the query and check it in its last 
`eventually`.



##########
sql/core/src/main/java/org/apache/spark/sql/execution/datasources/v2/ffi/NativeLibrary.java:
##########
@@ -903,6 +1143,260 @@ private static boolean isInterrupted(long task) {
     }
   }
 
+  /** The number of task contexts that are not closed yet, for tests. */
+  static int openTaskContexts() {
+    return tasks.size();
+  }
+
+  // 
---------------------------------------------------------------------------------------------
+  // Streams of engines
+  // 
---------------------------------------------------------------------------------------------
+
+  // The errno values that the callbacks of a stream return.
+  private static final int EINVAL = 22;
+  private static final int EIO = 5;
+
+  /**
+   * The callbacks of the streams that Spark wraps for engines, see {@link 
EngineStream}, the
+   * method handles that call the callbacks of the wrapped streams, and what 
the callbacks return
+   * for a stream that Spark does not know. They are created when the first 
stream is wrapped, so
+   * that a JVM that wraps no stream does not create them, and a failure to 
create them only fails
+   * the streams of engines.
+   */
+  private static final class EngineStreamCallbacks {
+    // The addresses of the C functions that call Java, which live as long as 
the JVM.
+    static final long GET_SCHEMA;
+    static final long GET_NEXT;
+    static final long GET_LAST_ERROR;
+    static final long RELEASE;
+    // The callbacks of struct ArrowArrayStream, called with the address of 
the function first:
+    // get_schema and get_next, get_last_error, and release.
+    static final MethodHandle INT_CALLBACK;
+    static final MethodHandle POINTER_CALLBACK;
+    static final MethodHandle VOID_CALLBACK;
+    // The message of get_last_error for a stream that Spark does not know, 
which lives as long as
+    // the JVM.
+    static final long UNKNOWN_STREAM_ERROR;
+
+    static {
+      try {
+        MethodHandles.Lookup lookup = MethodHandles.lookup();
+        // Linker.downcallHandle(FunctionDescriptor, Linker.Option...), whose 
method handles take
+        // the address of the function to call as their first argument.
+        MethodHandle unboundDowncall = lookup.findVirtual(
+          Class.forName("java.lang.foreign.Linker"), "downcallHandle",
+          MethodType.methodType(MethodHandle.class,
+            Class.forName("java.lang.foreign.FunctionDescriptor"), 
Ffm.NO_OPTIONS.getClass()))
+          .asFixedArity();
+        GET_SCHEMA = Ffm.upcall(
+          lookup.findStatic(NativeLibrary.class, "engineStreamGetSchema",
+            MethodType.methodType(int.class, long.class, long.class)),
+          C.I32, C.PTR, C.PTR);
+        GET_NEXT = Ffm.upcall(
+          lookup.findStatic(NativeLibrary.class, "engineStreamGetNext",
+            MethodType.methodType(int.class, long.class, long.class)),
+          C.I32, C.PTR, C.PTR);
+        GET_LAST_ERROR = Ffm.upcall(
+          lookup.findStatic(NativeLibrary.class, "engineStreamGetLastError",
+            MethodType.methodType(long.class, long.class)),
+          C.PTR, C.PTR);
+        RELEASE = Ffm.upcall(
+          lookup.findStatic(NativeLibrary.class, "engineStreamRelease",
+            MethodType.methodType(void.class, long.class)),
+          C.VOID, C.PTR);
+        INT_CALLBACK = callback(unboundDowncall, C.I32, C.PTR, C.PTR);
+        POINTER_CALLBACK = callback(unboundDowncall, C.PTR, C.PTR);
+        VOID_CALLBACK = callback(unboundDowncall, C.VOID, C.PTR);
+        UNKNOWN_STREAM_ERROR = toCString("The stream was released, or is not a 
stream of Spark.");
+      } catch (Throwable e) {
+        // The message names the cause, because the JVM only keeps this 
message for the later
+        // uses of the class.
+        throw new IllegalStateException(
+          "Cannot create the callbacks of the streams of engines: " + 
rootCause(e), e);
+      }
+    }
+
+    /**
+     * Returns a method handle that calls the C function at the address of its 
first argument, a
+     * {@code long}, with the given types, as {@link Ffm#downcall} does for a 
fixed address.
+     */
+    private static MethodHandle callback(MethodHandle unboundDowncall, C 
result, C... parameters)
+        throws Throwable {
+      MethodHandle handle = (MethodHandle) unboundDowncall.invoke(
+        Ffm.LINKER, Ffm.descriptor(result, parameters), Ffm.NO_OPTIONS);
+      handle = MethodHandles.filterArguments(handle, 0, Ffm.OF_ADDRESS);
+      return Ffm.convertPointers(handle, 1, Ffm.OF_ADDRESS, Ffm.ADDRESS_OF, 
result, parameters);
+    }
+  }
+
+  private static final AtomicLong nextEngineStreamId = new AtomicLong(1);
+  // The streams wrapped for engines that are not released yet, by their 
private_data.
+  private static final ConcurrentHashMap<Long, EngineStream> engineStreams =
+    new ConcurrentHashMap<>();
+
+  /**
+   * The stream of a read that an engine got with {@code 
SupportsArrowStreamRead}, which ties the
+   * task context of the read to the stream: the context is closed once the 
library released the
+   * stream, whenever and on whichever thread the engine releases it, also 
after the task ended.
+   * The stream of the library is moved to memory of Spark, and the stream of 
the engine gets
+   * callbacks of Spark, which call the current callbacks of the library's 
stream, and a
+   * private_data that identifies the wrapped stream, so that the engine can 
move the stream too.
+   */
+  private static final class EngineStream {
+    final long inner;
+    final Task task;
+    // The name of the data source, for the errors.
+    final String name;
+    // The message of the last error of Spark, rather than of the library, or 
0 if the last call
+    // returned an error of the library, or none. The callbacks never run 
concurrently, but may
+    // run on different threads one after the other.
+    volatile long sparkError;
+
+    EngineStream(long inner, Task task, String name) {
+      this.inner = inner;
+      this.task = task;
+      this.name = name;
+    }
+
+    long callback(long offset) {
+      return Platform.getLong(null, inner + offset);
+    }
+
+    /**
+     * Records an error of Spark in the given callback, which get_last_error 
then returns, and logs
+     * it. The message is built here, since building it can throw too, as a 
StackOverflowError on
+     * the small stack of a thread of an engine can.
+     */
+    int fail(String callback, Throwable e) {
+      try {
+        clearError();
+        String message =
+          "The " + callback + " of the stream of the native data source " + 
name + " failed";
+        sparkError = toCString(message + ": " + rootCause(e));
+        LOGGER.error(message, e);
+      } catch (Throwable ignored) {
+        // An upcall must not throw.
+      }
+      return EIO;
+    }
+
+    void clearError() {
+      if (sparkError != 0) {
+        Platform.freeMemory(sparkError);
+        sparkError = 0;
+      }
+    }
+  }
+
+  /**
+   * Wraps the stream at the given address, which read initialized with 
non-NULL callbacks, so that
+   * releasing it closes the given task context (see {@link EngineStream}). If 
it fails, the stream
+   * is left as it is.
+   *
+   * @param name the name of the data source, for the errors of the stream
+   */
+  static void wrapEngineStream(long stream, Task task, String name) {

Review Comment:
   **5. [Nit] Each call of an engine's stream goes through an upcall and a 
downcall**
   
   In [round 
1](https://github.com/apache/spark/pull/59934#discussion_r4417274537) I 
suggested replacing only `release`, and noted that wrapping all four callbacks 
costs an upcall per `get_next`. I still think that the wrapping is the more 
robust choice, since it needs no key from the library. Its cost is a bit larger 
than I wrote, though: each `get_schema`, `get_next` and `get_last_error` of the 
engine is an upcall into `engineStreamCall` and then a downcall back into the 
library. The first call on a thread of the engine also attaches that thread to 
the JVM, which, as far as I know, keeps it attached until the thread exits.
   
   The Javadoc and the guide say that the callbacks call into the JVM, but not 
what that costs an engine whose point is to stay out of it. Did you measure it 
against the import of a batch? If it is small, as I expect for batches of 
thousands of rows, could the guide say that each call goes through the JVM, and 
attaches a native thread to it? Since the wrapping is internal to Spark, it 
could still keep the callbacks of the library later, without a change of the 
interface.



##########
sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/v2/ffi/NativeDataSource.scala:
##########
@@ -350,25 +382,44 @@ class NativeDataSourceStreamReader(
     offset
   }
 
-  override def partitions(start: String, end: String): Array[InputPartition] = 
{
-    val partitions = call(name, "plan a scan of") {
-      library.streamPartitions(handle.get, start, end)
+  override def partitions(start: String, end: String): Array[InputPartition] = 
synchronized {
+    if (plannedRange != (start, end)) {
+      plannedPartitions = Try {
+        val planned = call(name, "plan a scan of") {
+          library.streamPartitions(handle.get, start, end)
+        }
+        val partitions = NativeInputPartition.from(planned.partitions)
+        // The executors fetch the state once for the micro-batch, as a 
broadcast, rather than with
+        // each of its tasks. Spark only plans the next range once the 
micro-batch of the previous
+        // one finished, so the copies of its state on the executors are 
removed then. The driver
+        // keeps its own copy, so that the partitions and the reader factories 
that an engine keeps
+        // can still fetch it, until the ContextCleaner removes it once 
nothing refers to them.
+        plannedState.foreach(_.unpersist(blocking = false))

Review Comment:
   **6. [Nit] A small state still gets a broadcast for each micro-batch**
   
   The broadcast answered [my item 1 of round 
5](https://github.com/apache/spark/pull/59934#discussion_r4420249991), which 
was about a large state sent with every task. For the state of a typical stream 
reader, such as a few offsets, each micro-batch now writes a broadcast to the 
block manager of the driver, registers it with the ContextCleaner, makes each 
executor fetch it once, and sends an unpersist to every executor when the next 
range is planned. With a short trigger interval, that may cost more than the 
bytes that it saves in each task.
   
   Could a state below some size, for example 64 KiB, stay in the partitions, 
with `NativeMicroBatchPartition` holding either the bytes or the broadcast? The 
test of a serialized partition would then use a state above that size.



##########
sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/v2/columnar/ColumnarTable.scala:
##########
@@ -222,9 +338,52 @@ abstract class ColumnarOnlyPartitionReaderFactory extends 
PartitionReaderFactory
   override def supportColumnarReads(partition: InputPartition): Boolean = true
 }
 
-class ColumnarStreamPartitionReaderFactory(reader: ColumnarStreamReader)
+object ColumnarProjection {
+  /**
+   * Returns `factory`, or a factory whose readers only return the columns at 
`projection` of the
+   * batches of the readers of `factory`, if any.
+   */
+  def apply(
+      factory: PartitionReaderFactory,
+      projection: Option[Array[Int]]): PartitionReaderFactory = projection 
match {
+    case Some(ordinals) => new ColumnarProjectionReaderFactory(factory, 
ordinals)
+    case None => factory
+  }
+}
+
+/**
+ * Reads only the columns at `ordinals` of the batches of the readers of 
`factory`, for a scan
+ * whose partitions return more columns than the scan reads.
+ */
+class ColumnarProjectionReaderFactory(factory: PartitionReaderFactory, 
ordinals: Array[Int])
   extends ColumnarOnlyPartitionReaderFactory {
   override def createColumnarReader(partition: InputPartition): 
PartitionReader[ColumnarBatch] = {
-    reader.read(partition)
+    new ColumnarProjectionReader(factory.createColumnarReader(partition), 
ordinals)
   }
 }
+
+private class ColumnarProjectionReader(
+    reader: PartitionReader[ColumnarBatch],
+    ordinals: Array[Int])
+  extends PartitionReader[ColumnarBatch] {
+
+  override def next(): Boolean = reader.next()
+
+  override def get(): ColumnarBatch = new ProjectedColumnarBatch(reader.get(), 
ordinals)

Review Comment:
   **9. [Nit] `ColumnarProjectionReader.get()` returns a new batch on each 
call**
   
   This comes from 870e3fa of #59931. `PartitionReader.get()` "should return 
same value until `next` is called", but this allocates a new 
`ProjectedColumnarBatch`, and an array of its columns, on each call, so two 
calls return different batches. Spark calls `get()` once for each `next()`, so 
this is only an allocation today, but could `next()` build the batch once, and 
`get()` return it?



##########
sql/core/src/main/java/org/apache/spark/sql/execution/datasources/v2/ffi/NativeLibrary.java:
##########
@@ -903,6 +1143,260 @@ private static boolean isInterrupted(long task) {
     }
   }
 
+  /** The number of task contexts that are not closed yet, for tests. */
+  static int openTaskContexts() {
+    return tasks.size();
+  }
+
+  // 
---------------------------------------------------------------------------------------------
+  // Streams of engines
+  // 
---------------------------------------------------------------------------------------------
+
+  // The errno values that the callbacks of a stream return.
+  private static final int EINVAL = 22;
+  private static final int EIO = 5;
+
+  /**
+   * The callbacks of the streams that Spark wraps for engines, see {@link 
EngineStream}, the
+   * method handles that call the callbacks of the wrapped streams, and what 
the callbacks return
+   * for a stream that Spark does not know. They are created when the first 
stream is wrapped, so
+   * that a JVM that wraps no stream does not create them, and a failure to 
create them only fails
+   * the streams of engines.
+   */
+  private static final class EngineStreamCallbacks {
+    // The addresses of the C functions that call Java, which live as long as 
the JVM.
+    static final long GET_SCHEMA;
+    static final long GET_NEXT;
+    static final long GET_LAST_ERROR;
+    static final long RELEASE;
+    // The callbacks of struct ArrowArrayStream, called with the address of 
the function first:
+    // get_schema and get_next, get_last_error, and release.
+    static final MethodHandle INT_CALLBACK;
+    static final MethodHandle POINTER_CALLBACK;
+    static final MethodHandle VOID_CALLBACK;
+    // The message of get_last_error for a stream that Spark does not know, 
which lives as long as
+    // the JVM.
+    static final long UNKNOWN_STREAM_ERROR;
+
+    static {
+      try {
+        MethodHandles.Lookup lookup = MethodHandles.lookup();
+        // Linker.downcallHandle(FunctionDescriptor, Linker.Option...), whose 
method handles take
+        // the address of the function to call as their first argument.
+        MethodHandle unboundDowncall = lookup.findVirtual(
+          Class.forName("java.lang.foreign.Linker"), "downcallHandle",
+          MethodType.methodType(MethodHandle.class,
+            Class.forName("java.lang.foreign.FunctionDescriptor"), 
Ffm.NO_OPTIONS.getClass()))
+          .asFixedArity();
+        GET_SCHEMA = Ffm.upcall(
+          lookup.findStatic(NativeLibrary.class, "engineStreamGetSchema",
+            MethodType.methodType(int.class, long.class, long.class)),
+          C.I32, C.PTR, C.PTR);
+        GET_NEXT = Ffm.upcall(
+          lookup.findStatic(NativeLibrary.class, "engineStreamGetNext",
+            MethodType.methodType(int.class, long.class, long.class)),
+          C.I32, C.PTR, C.PTR);
+        GET_LAST_ERROR = Ffm.upcall(
+          lookup.findStatic(NativeLibrary.class, "engineStreamGetLastError",
+            MethodType.methodType(long.class, long.class)),
+          C.PTR, C.PTR);
+        RELEASE = Ffm.upcall(
+          lookup.findStatic(NativeLibrary.class, "engineStreamRelease",
+            MethodType.methodType(void.class, long.class)),
+          C.VOID, C.PTR);
+        INT_CALLBACK = callback(unboundDowncall, C.I32, C.PTR, C.PTR);
+        POINTER_CALLBACK = callback(unboundDowncall, C.PTR, C.PTR);
+        VOID_CALLBACK = callback(unboundDowncall, C.VOID, C.PTR);
+        UNKNOWN_STREAM_ERROR = toCString("The stream was released, or is not a 
stream of Spark.");
+      } catch (Throwable e) {
+        // The message names the cause, because the JVM only keeps this 
message for the later
+        // uses of the class.
+        throw new IllegalStateException(
+          "Cannot create the callbacks of the streams of engines: " + 
rootCause(e), e);
+      }
+    }
+
+    /**
+     * Returns a method handle that calls the C function at the address of its 
first argument, a
+     * {@code long}, with the given types, as {@link Ffm#downcall} does for a 
fixed address.
+     */
+    private static MethodHandle callback(MethodHandle unboundDowncall, C 
result, C... parameters)
+        throws Throwable {
+      MethodHandle handle = (MethodHandle) unboundDowncall.invoke(
+        Ffm.LINKER, Ffm.descriptor(result, parameters), Ffm.NO_OPTIONS);
+      handle = MethodHandles.filterArguments(handle, 0, Ffm.OF_ADDRESS);
+      return Ffm.convertPointers(handle, 1, Ffm.OF_ADDRESS, Ffm.ADDRESS_OF, 
result, parameters);
+    }
+  }
+
+  private static final AtomicLong nextEngineStreamId = new AtomicLong(1);
+  // The streams wrapped for engines that are not released yet, by their 
private_data.
+  private static final ConcurrentHashMap<Long, EngineStream> engineStreams =
+    new ConcurrentHashMap<>();
+
+  /**
+   * The stream of a read that an engine got with {@code 
SupportsArrowStreamRead}, which ties the
+   * task context of the read to the stream: the context is closed once the 
library released the
+   * stream, whenever and on whichever thread the engine releases it, also 
after the task ended.
+   * The stream of the library is moved to memory of Spark, and the stream of 
the engine gets
+   * callbacks of Spark, which call the current callbacks of the library's 
stream, and a
+   * private_data that identifies the wrapped stream, so that the engine can 
move the stream too.
+   */
+  private static final class EngineStream {
+    final long inner;
+    final Task task;
+    // The name of the data source, for the errors.
+    final String name;
+    // The message of the last error of Spark, rather than of the library, or 
0 if the last call
+    // returned an error of the library, or none. The callbacks never run 
concurrently, but may
+    // run on different threads one after the other.
+    volatile long sparkError;
+
+    EngineStream(long inner, Task task, String name) {
+      this.inner = inner;
+      this.task = task;
+      this.name = name;
+    }
+
+    long callback(long offset) {
+      return Platform.getLong(null, inner + offset);
+    }
+
+    /**
+     * Records an error of Spark in the given callback, which get_last_error 
then returns, and logs
+     * it. The message is built here, since building it can throw too, as a 
StackOverflowError on
+     * the small stack of a thread of an engine can.
+     */
+    int fail(String callback, Throwable e) {
+      try {
+        clearError();
+        String message =
+          "The " + callback + " of the stream of the native data source " + 
name + " failed";
+        sparkError = toCString(message + ": " + rootCause(e));
+        LOGGER.error(message, e);

Review Comment:
   **8. [Nit] The errors of an engine's stream are logged without MDC**
   
   `log` logs the messages of the library with `MDC.of(LogKeys.PATH, ...)` and 
`MDC.of(LogKeys.MESSAGE, ...)` (L597-L604), but this line, and the one in 
`engineStreamRelease` (L1389-L1390), put the name of the data source into the 
message itself. Could they pass it as `MDC.of(LogKeys.DATA_SOURCE, name)`, and 
the callback as `MDC.of(LogKeys.FUNCTION_NAME, callback)`? For example:
   
   ```java
   LOGGER.error("The {} of the stream of the native data source {} failed", e,
     MDC.of(LogKeys.FUNCTION_NAME, callback), MDC.of(LogKeys.DATA_SOURCE, 
name));
   ```
   
   `sparkError` can keep the plain message.



##########
sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/v2/ffi/NativeDataSource.scala:
##########
@@ -970,21 +1207,43 @@ class NativeDataWriter(
 /** Creates the `SparkDsTaskContext` of the running task. */
 private[ffi] object NativeTask {
   /**
-   * @param partitionId the partition that the task reads or writes, by 
default the partition of
-   *                    the task
+   * @param partitionId the `partition_id` of the context: the index of the 
partition in the scan
+   *                    for a read, and by default the partition of the task
    * @param taskAttemptId the ID of the task attempt, by default the one of 
the task
+   * @param read whether the context is the one of a read, which the library 
may use after the
+   *             task ended: an engine may read the stream of a partition 
then, and a thread that
+   *             the task does not wait for may be in get_next once it 
completes, as with
+   *             `PipedRDD`. `is_interrupted` then also reports that the task 
failed or completed,
+   *             and the context does not keep the task alive. A data writer 
only runs within its
+   *             task, whose kill `is_interrupted` reports. Outside of a task, 
it reports nothing.
    */
   def create(
       partitionId: Option[Int] = None,
-      taskAttemptId: Option[Long] = None): NativeLibrary.Task = {
+      taskAttemptId: Option[Long] = None,
+      read: Boolean = false): NativeLibrary.Task = {
     val context = Option(TaskContext.get())
+    val interrupted: BooleanSupplier = context match {
+      case Some(taskContext) if read =>
+        // Set once the task failed or completed, by listeners, so that a 
check only reads
+        // volatile fields, unlike `isFailed()` and `isCompleted()`, which 
lock the task.
+        val ended = new AtomicBoolean(false)
+        taskContext.addTaskFailureListener((_, _) => ended.set(true))

Review Comment:
   **7. [Nit] Each read registers two listeners that stay until the end of its 
task**
   
   Thank you for the flag, which answers [item 6 of round 
7](https://github.com/apache/spark/pull/59934#discussion_r4463375838). Since 
every read now gets the context of a read, also Spark's own (L1074), a task 
that reads many partitions keeps two listeners and a flag for each until it 
ends, and runs them all then. For example, `coalesce(1)` of a scan with 100,000 
partitions keeps 200,000 listeners. That is the cost of [my item 11 of round 
1](https://github.com/apache/spark/pull/59934#discussion_r4417274621), now for 
Spark's own reads too.
   
   Since the flag only depends on the task, could the reads of a task share 
one? For example, a map by `taskAttemptId()` could hold the flag of each 
running task, whose single pair of listeners sets it and removes the entry once 
the task ended. The contexts of the reads keep their flag, so they still see it 
after that.



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


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

Reply via email to