HyukjinKwon commented on code in PR #59932:
URL: https://github.com/apache/spark/pull/59932#discussion_r4418773851


##########
sql/core/src/main/resources/org/apache/spark/sql/datasource/spark_datasource.h:
##########
@@ -56,47 +56,54 @@
  *
  * - Batch reads. On the driver, Spark creates a reader with create_reader, 
pushes operations down
  *   by calling, each at most once and in this order, push_limit and 
prune_columns, plans the
- *   partitions with partitions, serializes the state of the reader with 
serialize_reader, and
- *   closes it. Then it calls read on the executors, in one task for each 
partition. If Spark never
- *   plans the partitions, for example to only explain the plan of the query, 
it closes the reader
- *   once the reader is no longer referenced.
- * - Streaming reads. On the driver, Spark creates a stream reader with 
create_stream_reader and
- *   keeps it until the streaming query stops. For each micro-batch, it asks 
for the latest_offset,
- *   plans the partitions with stream_partitions and serializes the state with
- *   serialize_stream_reader, then calls read on the executors. Offsets are 
defined by the library,
- *   as compact JSON on a single line: Spark stores each offset as a line of 
the offset log of the
- *   checkpoint, so it rejects an offset that is not valid JSON or contains a 
line break. When a
- *   streaming query restarts, from its checkpoint, Spark creates a new stream 
reader and may call
- *   stream_partitions again for the last range that it planned before, so the 
partitions of a
- *   range must read the same data each time.
+ *   partitions with partitions, which also returns the state of the reader, 
and closes it. Then it
+ *   calls read on the executors, in one task for each partition, with the 
schema to read and the
+ *   state of the reader. If Spark never plans the partitions, for example to 
only explain the plan
+ *   of the query, it closes the reader once the reader is no longer 
referenced.
+ * - Streaming reads. On the driver, Spark creates a stream reader with 
create_stream_reader, with
+ *   the location of the checkpoint of the source, and keeps it until the 
streaming query stops. For
+ *   each micro-batch, or only before the first one with Trigger.AvailableNow, 
it asks for the
+ *   latest_offset, with the offset that the micro-batch starts after, plans 
the partitions with
+ *   stream_partitions, which also returns the state of the stream reader, 
then calls read on the
+ *   executors. Offsets are defined by the library, as compact JSON on a 
single line: Spark stores
+ *   each offset as a line of the offset log of the checkpoint, so it rejects 
an offset that is not
+ *   valid JSON or contains a line break. When a streaming query restarts, 
from its checkpoint,
+ *   Spark creates a new stream reader and may call stream_partitions again 
for the last range that
+ *   it planned before, so the partitions of a range must read the same data 
each time.
  * - Writes. On the driver, right before it starts the tasks of the write (or 
of a micro-batch),
- *   Spark creates a writer with create_writer, and serializes its state with 
serialize_writer.
- *   With adaptive query execution, Spark first runs the stages of the data to 
write that end with
- *   a shuffle, and creates no writer if they fail. The rest of the data, such 
as its last stage,
- *   is computed in the tasks of the write, so a writer may still be aborted 
because computing the
- *   data failed: do irreversible work, such as removing the existing data for 
an overwrite, in
- *   commit. On the executors, each task creates a data writer with 
create_data_writer, calls write
- *   for each batch of at most spark.sql.execution.arrow.maxRecordsPerBatch 
rows, which also ends
- *   once it holds spark.sql.execution.arrow.maxBytesPerBatch bytes, and calls 
commit_data_writer,
- *   or abort_data_writer if the task or the commit fails, before it closes 
the data writer. Then
- *   the driver calls commit with the messages of the tasks if they all 
succeeded, or abort
- *   otherwise, including when commit fails, and closes the writer. Several 
attempts of a task may
- *   write the same partition, after a failure or with speculative execution, 
each with a data
- *   writer of its own. Spark lets the attempts of a partition commit one at a 
time, and passes to
- *   commit the message of one committed attempt for each partition, so the 
write consists of the
- *   data of these attempts only. It aborts the data writers of the other 
attempts, except on an
- *   executor that is lost, where a data writer is neither committed nor 
aborted, and may even have
- *   committed before its message reached the driver. A streaming write 
commits or aborts each
- *   micro-batch (epoch). Spark may commit the same epoch more than once, for 
example when a
- *   streaming query restarts after a failure, so committing an epoch that is 
already committed
- *   must have no effect, for exactly-once output. The ID of the query, which 
create_writer gets for
- *   a streaming write, stays the same when the query restarts from its 
checkpoint, so the ID of the
- *   query and the epoch ID identify the output of a micro-batch, also when 
other queries write to
- *   the same destination. A micro-batch streaming query creates a new stream 
writer for each
- *   micro-batch, and Spark closes the previous stream writer of the query 
when the query creates a
- *   new one. Spark is not told when a streaming query stops, so it closes the 
last stream writer of
- *   the query once the writer is no longer referenced, or when the query is 
restarted. Streaming
- *   writes are micro-batch only: a query with continuous processing fails 
before create_writer.
+ *   Spark creates a writer with create_writer, which also returns the state 
of the writer. If Spark
+ *   rejects what create_writer returned, such as a state set more than once, 
it aborts the writer,
+ *   with the field rejected of SparkDsFinishWriteInfo set, before it closes 
it. With adaptive query
+ *   execution, Spark first runs the stages of the data to write that end with 
a shuffle, and
+ *   creates no writer if they fail. The rest of the data, such as its last 
stage, is computed in
+ *   the tasks of the write, so a writer may still be aborted because 
computing the data failed: do
+ *   irreversible work, such as removing the existing data for an overwrite, 
in commit. On the
+ *   executors, each task creates a data writer with create_data_writer, with 
the schema of the data
+ *   and the state of the writer, calls write for each batch of at most
+ *   spark.sql.execution.arrow.maxRecordsPerBatch rows, which also ends once 
it holds
+ *   spark.sql.execution.arrow.maxBytesPerBatch bytes, and calls 
commit_data_writer, or
+ *   abort_data_writer if the task or the commit fails before 
commit_data_writer succeeded, before
+ *   it closes the data writer. Then the driver calls commit with the messages 
of the tasks if they
+ *   all succeeded, or abort otherwise, including when commit fails, and 
closes the writer. Several

Review Comment:
   Done in 14a1d3a: the Order of the calls section now says that the data 
writers of the task attempts that Spark kills, such as speculative ones, may 
still be written to, aborted and closed after their writer is committed or 
aborted, and closed.



##########
sql/core/src/test/scala/org/apache/spark/sql/execution/datasources/v2/ffi/NativeDataSourceSuite.scala:
##########
@@ -706,14 +711,87 @@ class NativeDataSourceSuite extends QueryTest with 
SharedSparkSession {
         "action" -> "write to",
         "name" -> "native_sink",
         "msg" -> "commit_data_writer returned 2 values instead of one."))
-    // The task failed without aborting the committed data writer, which it 
closed, and the driver
-    // aborted the write.
-    assert(!new File(dir, "_ABORTED_COMMITTED_DATA_WRITER").exists())
+    // The task failed without aborting the committed data writer, which the 
test library would
+    // record as a violation of the order of the calls, and closed it. The 
driver aborted the write.
     checkDataWritersClosed(dir)
     assert(new File(dir, "_ABORTED").isFile)
     assert(!dir.listFiles().exists(_.getName.startsWith("part-")))
   }
 
+  nativeTest("keep the order of the calls whatever the callers of the adapter 
do") {

Review Comment:
   Done in 14a1d3a: `ColumnarDataSourceSuite` now has "prune the columns of a 
scan once" (with a counter of the `pruneColumns` calls of `TestRangePlanner`) 
and "do not abort a write whose commit succeeded" (for `ColumnarBatchWrite` and 
`ColumnarStreamingWrite`, with an abort without a commit as the control), which 
run on any Java. The native test still checks the side of the library.



##########
docs/sql-native-data-sources.md:
##########
@@ -101,32 +103,38 @@ Spark calls the functions of the table as follows:
   that the data source does not support before it computes the data to write. 
When the user does
   not specify a schema, Spark asks for it with `schema`.
 - **Batch reads**: on the driver, Spark creates a reader with `create_reader`, 
pushes operations
-  down with `push_limit` and `prune_columns`, plans the partitions with 
`partitions`, and
-  serializes the state of the reader with `serialize_reader`. Then it calls 
`read` on the
-  executors, in one task for each partition, and `read` exports an 
`ArrowArrayStream`. Its
-  callbacks can be called from any thread, but never concurrently.
-- **Streaming reads**: on the driver, Spark creates a stream reader with 
`create_stream_reader`.
-  For each micro-batch, it asks for the `latest_offset`, plans the partitions 
with
-  `stream_partitions`, and calls `read` on the executors. Offsets are defined 
by the library, as
-  compact JSON on a single line: Spark stores each offset as a line of the 
offset log of the
-  checkpoint, and rejects an offset that is not valid JSON or contains a line 
break, such as
-  pretty-printed JSON.
+  down with `push_limit` and `prune_columns`, and plans the partitions with 
`partitions`, which
+  also returns the state of the reader. Then it calls `read` on the executors, 
in one task for each
+  partition, with a `SparkDsReadInfo` that has the partition, the schema to 
read and the state of
+  the reader, and `read` exports an `ArrowArrayStream`. Its callbacks can be 
called from any
+  thread, but never concurrently.
+- **Streaming reads**: on the driver, Spark creates a stream reader with 
`create_stream_reader`,
+  which gets the location of the checkpoint of the source in a 
`SparkDsStreamReaderInfo`. For each
+  micro-batch, it asks for the `latest_offset`, with the offset that the 
micro-batch starts after,
+  plans the partitions with `stream_partitions`, which also returns the state 
of the stream reader,
+  and calls `read` on the executors. Offsets are defined by the library, as 
compact JSON on a
+  single line: Spark stores each offset as a line of the offset log of the 
checkpoint, and rejects
+  an offset that is not valid JSON or contains a line break, such as 
pretty-printed JSON.
 - **Writes**: on the driver, right before the tasks of the write (or of a 
micro-batch) start, Spark
   creates a writer with `create_writer`, which gets a `SparkDsWriteInfo` with 
the mode of the write,
   `SPARK_DS_WRITE_APPEND` or `SPARK_DS_WRITE_TRUNCATE`, and the ID of the 
query of a streaming
-  write. With adaptive query execution, it first runs the shuffle stages of 
the data to write, and
-  creates no writer if they fail, but the last stage runs in the tasks of the 
write, so a writer can
-  still be aborted because computing the data failed: do irreversible work, 
such as removing the
-  existing data for an overwrite, in `commit`. On the executors, each task 
creates a data writer
-  with `create_data_writer`, writes the batches of its data with `write`, and 
calls
-  `commit_data_writer`, or `abort_data_writer` if the task or the commit 
fails. Then the driver
-  calls `commit` with the messages of the tasks, or `abort` if the write 
failed. A streaming write
-  commits each micro-batch with its epoch ID, and the ID of the query stays 
the same when the query
-  restarts from its checkpoint: together, they identify the output of a 
micro-batch, so that a
-  library can skip an epoch that it already committed. A micro-batch streaming 
query creates a new
-  stream writer for each micro-batch, and Spark closes the previous one when 
it creates the next
-  one. Because Spark is not told when a streaming query stops, the last stream 
writer of a query is
-  closed once it is no longer referenced, or when the query is restarted.
+  write, and returns the state of the writer. With adaptive query execution, 
it first runs the
+  shuffle stages of the data to write, and creates no writer if they fail, but 
the last stage runs
+  in the tasks of the write, so a writer can still be aborted because 
computing the data failed: do
+  irreversible work, such as removing the existing data for an overwrite, in 
`commit`. On the
+  executors, each task creates a data writer with `create_data_writer`, which 
gets the schema of the
+  data and the state of the writer in a `SparkDsDataWriterInfo`, writes the 
batches of its data with
+  `write`, and calls `commit_data_writer`, or `abort_data_writer` if the task 
or the commit fails.
+  Then the driver calls `commit` with the messages of the tasks, or `abort` if 
the write failed.

Review Comment:
   Done in 14a1d3a: the Writes item of the guide now uses the suggested text.



##########
sql/core/src/test/scala/org/apache/spark/sql/execution/datasources/v2/ffi/NativeDataSourceTestUtils.scala:
##########
@@ -37,6 +37,29 @@ object NativeDataSourceTestUtils {
 
   private val libraries = new ConcurrentHashMap[String, File]()
 
+  /**
+   * The file to which the test libraries append the calls that the order of 
the calls of the
+   * header rules out, one per line. It does not exist as long as there are 
none.
+   */
+  lazy val orderViolations: File = new File(buildDir, "order_violations")
+
+  /**
+   * Runs a test that uses the test libraries, and fails it if the libraries 
got calls that the
+   * order of the calls of the header rules out. The calls recorded before, 
such as by a task of an
+   * earlier test that was still being killed, are dropped.
+   */
+  def checkingOrderOfCalls[T](f: => T): T = {
+    orderViolations.delete()
+    val result = f

Review Comment:
   Done in 14a1d3a: `checkingOrderOfCalls` waits until the `statusTracker` 
reports no running task before it reads the file, and when the test fails, it 
adds the recorded violations to its error as suppressed.



##########
sql/core/src/test/resources/native-datasource/test_native_datasource.cc:
##########
@@ -1033,28 +1162,36 @@ int planPartitions(void* handle, SparkDsSink 
add_partition, void* sink) {
       int64_t stop = std::min(hi, start + step);
       sendBytes(add_partition, sink, std::to_string(start) + "," + 
std::to_string(stop));
     }
+    sendState(reader->state, set_state, sink, serialize(reader->state));
   });
 }
 
-int serializeReader(void* handle, SparkDsSink set_state, void* sink) {
-  return guarded([&] { sendBytes(set_state, sink, 
serialize(fromHandle<Reader>(handle)->state)); });
-}
-
 void closeReader(void* handle) { delete fromHandle<Reader>(handle); }
 
 #ifndef READ_ONLY
 
-int initialOffset(void*, SparkDsSink set_offset, void* sink) {
-  return guarded([&] { sendBytes(set_offset, sink, "{\"offset\":0}"); });
+int initialOffset(void*, const SparkDsInitialOffsetInfo* info, SparkDsSink 
set_offset,

Review Comment:
   Done in 14a1d3a: `initial_offset` now goes through `tracked`, and the option 
`fail` has the steps `push_limit`, `prune_columns`, `initial_offset`, 
`latest_offset`, `stream_partitions` and `commit_offset`. The new test "call no 
other function on a reader whose call failed than its close function" makes 
each of them fail, and checks that the stream reader is closed afterwards.



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