spark-test commented on code in PR #59942:
URL: https://github.com/apache/spark/pull/59942#discussion_r4420070124


##########
sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/v2/columnar/ColumnarTable.scala:
##########
@@ -189,11 +220,73 @@ class ColumnarBatchScan(planner: ColumnarBatchPlanner) 
extends Batch {
 
 case class ColumnarStreamOffset(json: String) extends Offset
 
-class ColumnarMicroBatchStream(reader: ColumnarStreamReader) extends 
MicroBatchStream {
+/**
+ * The micro-batch stream of a [[ColumnarDataSource]]. The reader gets the 
offset that each
+ * micro-batch starts after, the end of the last one, for every trigger, and 
the limit on the data
+ * of a micro-batch, its default read limit, all the available data unless it 
limits it. With
+ * `Trigger.AvailableNow`, Spark runs micro-batches up to the first latest 
offset without a limit
+ * that Spark asks for, rather than wrapping the stream in one that asks for 
the latest offset from
+ * the initial offset: in one micro-batch, or within the limit in several.
+ *
+ * @param checkpointLocation the location of the checkpoint of the source, 
where the stream keeps
+ *                           the initial offset, under 
`ColumnarMicroBatchStream.InitialOffsetLog`
+ */
+class ColumnarMicroBatchStream(reader: ColumnarStreamReader, 
checkpointLocation: String)
+  extends MicroBatchStream with SupportsTriggerAvailableNow {
+
+  // The latest offset without a limit that the reader returned last, which 
the progress of the
+  // query reports.
+  @volatile private var lastLatestOffset: Offset = _
+  // The limit on the data of each micro-batch, asked once.
+  private lazy val readLimit = reader.defaultReadLimit()

Review Comment:
   **Nit (P3):** When `reader.defaultReadLimit()` throws here, because 
`default_read_limit` fails or returns an unknown kind or a `max_rows` below 1, 
the query fails without ever stopping this stream. 
`MicroBatchExecution.logicalPlan` asks for the limit while it assigns 
`uniqueSources` (MicroBatchExecution.scala:385-413), after `toMicroBatchStream` 
has already created the native stream reader (line 301). 
`StreamExecution.stopSources` only stops `uniqueSources`, which is still empty 
when its assignment throws (StreamExecution.scala:205 and 570-580), so `stop()` 
never runs, and `close_stream_reader` waits for the `NativeHandle` Cleaner, 
once the failed query is no longer referenced.
   
   The header says that Spark releases a stream reader when the streaming query 
stops, so a library whose reader holds connections, threads or files keeps them 
past the failed start, one reader per attempt when a driver retries a 
misconfigured query. Only the rejection path of a data source with 
`SPARK_DS_ADMISSION_CONTROL` is affected, but under every micro-batch trigger, 
since `Trigger.Once` and the `Trigger.AvailableNow` fallback still ask for the 
limit. The rejection test only checks the error.
   
   Stopping the reader before rethrowing would release it, for any 
`ColumnarStreamReader`, for example as `createWriter` in `NativeLibrary` does:
   
   ```scala
     private lazy val readLimit =
       try reader.defaultReadLimit() catch {
         case e: Throwable =>
           // The query fails before Spark records the streams to stop, so stop 
this one here.
           try reader.stop() catch { case stopError: Throwable => 
e.addSuppressed(stopError) }
           throw e
       }
   ```
   
   **Verification:**
   - **Regression:** Start a streaming query on a native source whose 
default_read_limit is rejected, both for an unknown kind and for a maximum of 0 
rows, with the test library's closed-path marker. Check that the query still 
fails with the same NATIVE_DATA_SOURCE_ERROR, and that the stream reader was 
closed when the query terminated, without relying on garbage collection.
   - **Behavior:** Check that queries with a valid default read limit, and 
sources without admission control, still read the same micro-batches and close 
their stream reader only when the query stops.



##########
docs/sql-native-data-sources.md:
##########
@@ -172,17 +207,19 @@ regularly and use timeouts. The task reaper, 
`spark.task.reaper.enabled` and
 ### Compatibility
 
 The function table, `SparkDsLibrary`, and the structs that Spark passes to the 
library,
-`SparkDsHost`, `SparkDsCreateInfo`, `SparkDsTaskContext` and 
`SparkDsWriteInfo`, start with their
-size, so that they can grow. The version of the interface, 1, is 
`SPARK_DS_ABI_VERSION`, which the
-table and the manifest of a package declare. Within a version:
+`SparkDsHost`, `SparkDsTaskContext` and those whose names end with `Info`, 
such as

Review Comment:
   **Nit (P3):** `SparkDsReadLimit` also starts with its size and is passed to 
the library, as the argument of `default_read_limit` and through 
`SparkDsLatestOffsetInfo.limit`, but its name does not end with `Info`, so this 
list leaves it out, and the guide never names it. It is also the one struct 
that a library writes into, while the rules below only say that a library reads 
the fields that fit in the size of a struct that Spark passes. The header has 
the matching rule at the struct: "In default_read_limit, the library sets the 
fields that fit in the size that Spark set." Following only the guide, a 
library built against a later version-1 header, whose `SparkDsReadLimit` has 
grown, could fill its own larger struct into the smaller one that an earlier 
Spark allocated.
   
   Could this list include `SparkDsReadLimit`, or say "the structs that Spark 
passes to the library" as the header does, and the rules add that 
`default_read_limit` only sets the fields that fit in the size that Spark set?
   
   **Verification:**
   - **Inspection:** Check that the Compatibility section of the native data 
source guide covers every size-prefixed struct that Spark passes to a library, 
including SparkDsReadLimit, and states both the read-side and the write-side 
size rules consistently with spark_datasource.h.



##########
sql/core/src/test/scala/org/apache/spark/sql/execution/datasources/v2/ffi/NativeDataSourceSuite.scala:
##########
@@ -911,6 +1162,49 @@ class NativeDataSourceSuite extends QueryTest with 
SharedSparkSession {
         "msg" -> "create failed with the error code 1."))
   }
 
+  nativeTest("reject a state returned more than once") {

Review Comment:
   **Nit (P3):** This covers a state set twice, but no test covers a state that 
is never set, which is how a stateless reader or writer now says that it has 
none, as the header's optional functions convention says and the guide's Rust 
and C++ examples do; no test runs those examples. `NativeLibrary.state` returns 
null for zero states, and the callers turn it into an empty state with 
`bytesOrEmpty(Option(...))`. But `test_native_datasource.cc` always sends a 
state unless `states` says otherwise, and its `read` and `create_data_writer` 
fail without one, since they check the kind and the fields kept in it. So a 
change that rejected a missing state, or passed null to the executors, would 
break every stateless library while this suite still passes.
   
   Could the test library get a stateless mode, in which `partitions`, 
`stream_partitions` and `create_writer` never call `set_state`, and `read` and 
`create_data_writer` do not need a state?
   
   **Verification:**
   - **Regression:** Run a batch read, a micro-batch streaming read and a batch 
write against a native library whose partitions, stream_partitions and 
create_writer never call set_state. Check that they return the expected rows 
and that read and create_data_writer get a state of length 0, so that any 
change that rejects or mishandles a missing state fails.



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