gengliangwang commented on code in PR #59961:
URL: https://github.com/apache/spark/pull/59961#discussion_r4445873980


##########
sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/v2/ffi/NativeDataSource.scala:
##########
@@ -858,9 +897,21 @@ class NativePartitionReader(
   override def close(): Unit = synchronized {
     if (!closed) {
       closed = true
-      if (!reading) {
+      // Another thread may be in next, or still read the batch that next 
returned to it.
+      if (!reading && consumerDone) {

Review Comment:
   Good point, and I agree: the Cleaner should only cover the case it exists 
for. In the next revision, if `next()` has returned false (the end of the 
stream), `close()` frees everything right away: the stream, the dictionaries, 
the task, the last batch's buffers and the child allocator. The Cleaner only 
takes the buffers when `close()` runs while a batch that `next()` returned may 
still be read, for example with a LIMIT or when a script exits early. That 
covers the normal fully-read close from `DataSourceRDD`. Once a reader has 
handed back the end of the stream, `PartitionReader` gives `get()` no valid 
batch, and every consumer I know of stops reading the batch by then. Script 
transformation is one example: after its first batch, the writer thread is the 
one that calls `hasNext`. The exception is the one you name. 
`NativeColumnVector` extends `ReusedArrowColumnVector` and therefore 
`ArrowColumnVector`, so `ColumnarArrowEvalPythonEvaluatorFactory` picks path 1 
for it (with `spark.sql
 .execution.arrow.pythonUDF.columnarInput.enabled`), and path 1 holds 
references to the pass-through columns until Python returns the results. Path 1 
is already wrong for this reader before the end of the stream, though, because 
the vectors are reused: the pass-through columns of batch N would show batch 
N+1. So the fallback I plan sends batches whose columns are 
`ReusedArrowColumnVector`s down path 2. Path 2 copies the pass-through rows 
into the `HybridRowQueue` before the writer asks for the next batch, so nothing 
reads the reader's buffers after `next()` returns false. With that in place, 
freeing at the end of the stream is safe. I'll add a test that a reader read to 
the end and then closed frees its buffers without waiting for a GC.
   
   <sub>Automated reply (Opus)</sub>



##########
sql/core/src/test/scala/org/apache/spark/sql/execution/datasources/v2/ffi/NativeDataSourceSuite.scala:
##########
@@ -1256,6 +1256,81 @@ class NativeDataSourceSuite extends QueryTest with 
SharedSparkSession {
     }
   }
 
+  nativeTest("close a reader while another thread reads the batch that it 
returned") {
+    val session = newSession(defaultPackage)
+    val pool = ThreadUtils.newDaemonSingleThreadExecutor("native-reader")
+    val marker = new File(Utils.createTempDir(), "released")
+    // get_next does not wait: the file .continue exists.
+    new File(marker.getPath + ".continue").createNewFile()
+    val df = range(session, "end" -> "10", "partitions" -> "1", "slow_read" -> 
marker.getPath)
+    val batch = scan(df.select("id")).toBatch
+    val reader = 
batch.createReaderFactory().createColumnarReader(batch.planInputPartitions().head)
+    try {
+      def onPool[T](f: => T): T = pool.submit(new Callable[T] {
+        override def call(): T = f
+      }).get(1, TimeUnit.MINUTES)
+      assert(onPool(reader.next()))
+      // The other thread may still read the batch, whose buffers the reader 
keeps until that
+      // thread calls next again.
+      reader.close()
+      assert(!marker.isFile, "The stream was released.")
+      // Nor does a next or another close on this thread, which did not read 
the batch.
+      assert(!reader.next())
+      reader.close()
+      assert(!marker.isFile, "The stream was released.")
+      val rows = onPool((0 until 
reader.get().numRows()).map(reader.get().column(0).getLong))
+      assert(rows === (0L until 10L))
+      assert(!onPool(reader.next()))
+      assert(Files.readString(marker.toPath) == "after get_next")
+    } finally {
+      // Once the other thread ends, a close that the test did not reach 
releases the reader.
+      pool.shutdown()
+      pool.awaitTermination(1, TimeUnit.MINUTES)
+      Utils.tryLogNonFatalError(reader.close())
+    }
+  }
+
+  nativeTest("release a closed reader when the thread that read its batch 
closes it or ends") {
+    val session = newSession(defaultPackage)
+    def onPool[T](pool: ExecutorService)(f: => T): T = pool.submit(new 
Callable[T] {
+      override def call(): T = f
+    }).get(1, TimeUnit.MINUTES)
+    // Returns a reader closed while a thread of the pool may still read its 
batch, and the file
+    // that the library creates when the stream is released.
+    def closedReader(pool: ExecutorService) = {

Review Comment:
   Done in 9b9dfb979474: Gave `closedReader` the explicit return type 
`(PartitionReader[ColumnarBatch], File)` and added the two imports it needs.
   
   <sub>Automated reply (Opus)</sub>



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