HyukjinKwon commented on code in PR #59934:
URL: https://github.com/apache/spark/pull/59934#discussion_r4417793209
##########
sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/v2/ffi/NativeDataSource.scala:
##########
@@ -278,37 +284,69 @@ class NativeBatchPlanner(
override def plan(): ColumnarPlannedBatch = {
try {
- val partitions = call(name, "plan a scan
of")(library.partitions(handle.get))
- val readerState = bytesOrEmpty(
- call(name, "plan a scan
of")(optional(library.serializeReader(handle.get))))
+ val planned = call(name, "plan a scan
of")(library.partitions(handle.get))
ColumnarPlannedBatch(
- NativeInputPartition.from(partitions),
- new NativePartitionReaderFactory(executorLocation, name, readerState,
readSchema))
+ NativeInputPartition.from(planned.partitions),
+ new NativePartitionReaderFactory(executorLocation, name,
+ bytesOrEmpty(Option(planned.state)), readSchema, timeZoneId))
} finally {
- // The executors only need the serialized state.
+ // The executors only need the state that the reader returned.
handle.close()
}
}
}
-/** Reads the partitions of a planned batch scan of a native data source, on
the executors. */
+/**
+ * Reads the partitions of a planned batch scan of a native data source, on
the executors. An
+ * engine can also get the Arrow C stream of a partition from it, see
[[SupportsArrowStreamRead]].
+ */
class NativePartitionReaderFactory(
location: NativeLibraryLocation,
name: String,
readerState: Array[Byte],
- readSchema: StructType)
- extends ColumnarOnlyPartitionReaderFactory {
+ readSchema: StructType,
+ timeZoneId: String)
+ extends ColumnarOnlyPartitionReaderFactory with SupportsArrowStreamRead {
override def createColumnarReader(partition: InputPartition):
PartitionReader[ColumnarBatch] = {
- new NativePartitionReader(
- location, name, readerState,
partition.asInstanceOf[NativeInputPartition].bytes, readSchema)
+ new NativePartitionReader(location, name, NativeLibrary.READ_BATCH,
readerState,
+ partition.asInstanceOf[NativeInputPartition].bytes, readSchema,
timeZoneId)
+ }
+
+ override def openArrowStream(partition: InputPartition, streamAddress:
Long): Unit = {
+ val context = TaskContext.get()
+ if (context == null || context.isCompleted()) {
+ throw new IllegalStateException(
+ "openArrowStream must be called in the running task that reads the
partition.")
+ }
+ 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 nativePartition = partition.asInstanceOf[NativeInputPartition]
+ // The partition of the scan: the task of an engine may read others than
its own.
+ val task = NativePartitionReader.openStream(location, name,
NativeLibrary.READ_BATCH,
+ readerState, nativePartition.bytes, readSchema, timeZoneId,
streamAddress,
+ Some(nativePartition.index))
Review Comment:
Done in 173c264: the context of an engine's stream reports `is_interrupted`
as `isInterrupted() || isFailed() || isCompleted()` of its task. Spark's own
reader keeps reporting only a kill. The test opens a `wait_for_interrupt`
stream in a task and reads it after the task completed: `get_next` sees the
interruption right away.
##########
sql/core/src/main/java/org/apache/spark/sql/execution/datasources/v2/ffi/NativeLibrary.java:
##########
@@ -903,6 +1053,163 @@ private static boolean isInterrupted(long task) {
}
}
+ //
---------------------------------------------------------------------------------------------
+ // Streams of engines
+ //
---------------------------------------------------------------------------------------------
+
+ // The offsets of the fields of struct ArrowArrayStream, and its size.
+ private static final long STREAM_GET_SCHEMA = 0;
+ private static final long STREAM_GET_NEXT = 8;
+ private static final long STREAM_GET_LAST_ERROR = 16;
+ private static final long STREAM_RELEASE = 24;
+ private static final long STREAM_PRIVATE_DATA = 32;
+ private static final long STREAM_SIZE = 40;
+ // The errno values that the callbacks of a stream return.
+ private static final int EINVAL = 22;
+ private static final int EIO = 5;
+
+ 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 callbacks of the streams of the libraries, by address, for each
callback of a stream: a
+ // library typically has one of each, for all its streams.
+ private static final ConcurrentHashMap<Long, MethodHandle>
getSchemaCallbacks =
+ new ConcurrentHashMap<>();
+ private static final ConcurrentHashMap<Long, MethodHandle> getNextCallbacks =
+ new ConcurrentHashMap<>();
+ private static final ConcurrentHashMap<Long, MethodHandle>
getLastErrorCallbacks =
+ new ConcurrentHashMap<>();
+ private static final ConcurrentHashMap<Long, MethodHandle> releaseCallbacks =
+ 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 those of the library, 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;
+ final MethodHandle getSchema;
+ final MethodHandle getNext;
+ final MethodHandle getLastError;
+ final MethodHandle release;
+
+ EngineStream(long inner, Task task) throws Throwable {
+ this.inner = inner;
+ this.task = task;
+ getSchema = callback(getSchemaCallbacks, inner + STREAM_GET_SCHEMA,
C.I32, C.PTR, C.PTR);
+ getNext = callback(getNextCallbacks, inner + STREAM_GET_NEXT, C.I32,
C.PTR, C.PTR);
+ getLastError = callback(getLastErrorCallbacks, inner +
STREAM_GET_LAST_ERROR, C.PTR, C.PTR);
+ release = callback(releaseCallbacks, inner + STREAM_RELEASE, C.VOID,
C.PTR);
+ }
+
+ private static MethodHandle callback(
+ ConcurrentHashMap<Long, MethodHandle> callbacks,
+ long field,
+ C result,
+ C... parameters) throws Throwable {
+ long address = Platform.getLong(null, field);
+ MethodHandle handle = callbacks.get(address);
+ if (handle == null) {
+ handle = Ffm.downcall(Ffm.OF_ADDRESS.invoke(address), result,
parameters);
+ callbacks.putIfAbsent(address, handle);
+ }
Review Comment:
Done in 173c264: `Ffm` has three downcall handles that take the address of
the function first (`Linker.downcallHandle(FunctionDescriptor,
Linker.Option...)`), and each upcall reads the current pointer from `inner`.
The four maps, `callback` and the handle fields are gone.
##########
sql/core/src/test/scala/org/apache/spark/sql/execution/datasources/v2/ffi/NativeDataSourceSuite.scala:
##########
@@ -1145,6 +1222,105 @@ class NativeDataSourceSuite extends QueryTest with
SharedSparkSession {
check(writeTasks)
}
+ nativeTest("open the partitions of a batch scan as Arrow C streams") {
+ val session = newSession(defaultPackage)
+ // The reader factory and the partitions of the batch scan of a query,
which an engine gets
+ // from its plan.
+ def plannedScan(df: DataFrame): (SupportsArrowStreamRead,
Seq[InputPartition]) = {
+ val scan = df.queryExecution.executedPlan.collectFirst { case s:
BatchScanExec => s }.get
+ (scan.readerFactory.asInstanceOf[SupportsArrowStreamRead],
scan.inputPartitions)
+ }
+ // Runs `f` in a task that only gets the files of the session.
+ def inTask[T: ClassTag](f: => T): T =
session.artifactManager.withResources {
+ session.sparkContext.parallelize(Seq(0), 1).map(_ => f).collect().head
+ }
+ import TestArrowEngine.readIds
+
+ // An engine reads both partitions of the scan in a single task.
+ val taskDir = Utils.createTempDir()
+ val (factory, partitions) =
+ plannedScan(range(session, "end" -> "6", "task_path" ->
taskDir.getPath).select("id"))
+ assert(partitions.length == 2)
+ val ids = inTask {
+ partitions.flatMap { partition =>
+ val allocator = ArrowUtils.rootAllocator.newChildAllocator("engine",
0, Long.MaxValue)
+ try {
+ val stream = ArrowArrayStream.allocateNew(allocator)
+ try factory.openArrowStream(partition, stream.memoryAddress()) catch
{
+ case e: Throwable =>
+ stream.close()
+ throw e
+ }
+ readIds(stream, allocator)
+ } finally {
+ allocator.close()
+ }
+ }
+ }
+ assert(ids.sorted == (0L until 6L))
+ // The library got the partition of the scan that it reads, not the one of
the task.
+ val tasks = taskDir.listFiles().map { file =>
+ file.getName.toInt ->
Files.readString(file.toPath).split(",").map(_.toLong).toSeq
+ }.toMap
+ assert(tasks.keySet == Set(0, 1) && tasks.values.map(_(5)).toSet.size ==
1, tasks)
+ assert(NativeLibrary.openEngineStreams() == 0)
+
+ // The engine may release the stream after its task ended, and on another
thread: Spark frees
+ // the context of the task once the library released the stream.
+ val released = new File(Utils.createTempDir(), "released")
+ val (laterFactory, laterPartitions) =
+ plannedScan(range(session, "end" -> "6", "released_path" ->
released.getPath).select("id"))
+ val address = inTask {
+ val stream = Platform.allocateMemory(40)
+ Platform.setMemory(stream, 0.toByte, 40)
+ laterFactory.openArrowStream(laterPartitions.head, stream)
+ stream
+ }
+ assert(NativeLibrary.openEngineStreams() == 1 && !released.exists())
+ assert(readIds(ArrowArrayStream.wrap(address), ArrowUtils.rootAllocator)
== (0L until 3L))
+ assert(NativeLibrary.openEngineStreams() == 0 && released.exists())
Review Comment:
Done in 173c264: `NativeLibrary.openTaskContexts()` counts the contexts in
`tasks`. The test checks that the count is one higher after the task ended with
the stream unreleased, and back after `readIds` released it.
##########
sql/core/src/main/resources/org/apache/spark/sql/datasource/spark_datasource.h:
##########
@@ -304,9 +308,36 @@ typedef struct SparkDsCreateInfo {
int64_t num_options;
} SparkDsCreateInfo;
+/* 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
+ * URI, possibly of a file system that the library cannot access, such as
s3a or hdfs.
+ */
+ const char* checkpoint_location;
+} SparkDsStreamReaderInfo;
+
+/* 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. With Trigger.AvailableNow,
Spark asks for the
+ * latest offset only once, when the query starts, with the initial offset.
+ */
+ const char* start;
+} SparkDsLatestOffsetInfo;
+
/*
* 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. An
engine that runs a query
+ * in native code may get the stream of read from Spark, and release it once
the task ended, from
+ * another thread: the task stays valid until then too.
Review Comment:
Done in 173c264: the comment of `SparkDsTaskContext` says that an engine may
keep the arrays of the stream after it released the stream, so that the release
of an array must not use the stream or the task.
##########
sql/core/src/main/java/org/apache/spark/sql/execution/datasources/v2/ffi/NativeLibrary.java:
##########
@@ -903,6 +1053,163 @@ private static boolean isInterrupted(long task) {
}
}
+ //
---------------------------------------------------------------------------------------------
+ // Streams of engines
+ //
---------------------------------------------------------------------------------------------
+
+ // The offsets of the fields of struct ArrowArrayStream, and its size.
+ private static final long STREAM_GET_SCHEMA = 0;
+ private static final long STREAM_GET_NEXT = 8;
+ private static final long STREAM_GET_LAST_ERROR = 16;
+ private static final long STREAM_RELEASE = 24;
+ private static final long STREAM_PRIVATE_DATA = 32;
+ private static final long STREAM_SIZE = 40;
+ // The errno values that the callbacks of a stream return.
+ private static final int EINVAL = 22;
+ private static final int EIO = 5;
+
+ 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 callbacks of the streams of the libraries, by address, for each
callback of a stream: a
+ // library typically has one of each, for all its streams.
+ private static final ConcurrentHashMap<Long, MethodHandle>
getSchemaCallbacks =
+ new ConcurrentHashMap<>();
+ private static final ConcurrentHashMap<Long, MethodHandle> getNextCallbacks =
+ new ConcurrentHashMap<>();
+ private static final ConcurrentHashMap<Long, MethodHandle>
getLastErrorCallbacks =
+ new ConcurrentHashMap<>();
+ private static final ConcurrentHashMap<Long, MethodHandle> releaseCallbacks =
+ 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 those of the library, 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;
Review Comment:
Done in 173c264: the supplier holds the `TaskContext` through a
`WeakReference`, and reports `true` once it is gone.
--
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]