andygrove commented on code in PR #6219:
URL: https://github.com/apache/datafusion-comet/pull/6219#discussion_r4105657388


##########
native/core/src/execution/jni_api.rs:
##########
@@ -1008,17 +1005,62 @@ fn prepare_output(
 /// Because the input source could be another native execution stream, which
 /// will be executed in another tokio blocking thread. It causes JNI throw
 /// Java exception. So we pull input batches here and insert them into scan
-/// operators before polling the stream,
+/// operators before polling the stream. Returns whether any scan made a JNI 
call.
 #[inline]
-fn pull_input_batches(exec_context: &mut ExecutionContext) -> Result<(), 
CometError> {
-    exec_context.scans.iter_mut().try_for_each(|scan| {
-        scan.get_next_batch()?;
-        Ok::<(), CometError>(())
-    })?;
-    exec_context.shuffle_scans.iter_mut().try_for_each(|scan| {
-        scan.get_next_batch()?;
-        Ok::<(), CometError>(())
+fn pull_input_batches(exec_context: &mut ExecutionContext) -> Result<bool, 
CometError> {
+    let mut pulled = false;
+    for scan in exec_context.scans.iter_mut() {
+        pulled |= scan.get_next_batch()?;
+    }
+    for scan in exec_context.shuffle_scans.iter_mut() {
+        pulled |= scan.get_next_batch()?;
+    }
+    Ok(pulled)
+}
+
+/// Yields once, so the `block_on` thread sleeps until a waker registered by 
an earlier poll
+/// fires: a JVM-fed scan refilled by `pull_input_batches`, or native I/O that 
completed.
+async fn park_until_woken() {
+    let mut polled = false;
+    poll_fn(|_| {
+        if std::mem::replace(&mut polled, true) {
+            Poll::Ready(())
+        } else {
+            Poll::Pending
+        }
     })
+    .await
+}
+
+/// Drives `stream` to its next item. JVM-fed scans return `Pending` until 
`on_pending` refills
+/// them, so every pending poll runs it. `on_pending` returns whether it made 
a JNI call, and the
+/// loop parks only when it did not, because the stream is then waiting on 
native I/O.
+///
+/// After a JNI call the loop polls again without parking. The call can run 
another Comet plan on
+/// this thread, as when a native writer's input is itself native, and that 
plan's `block_on`
+/// shares this thread's parker. If the native I/O completes while the nested 
plan is parked, the
+/// nested park takes the wake-up, and a park here would wait for a wake that 
has already fired.
+async fn next_batch<S>(
+    stream: &mut S,
+    mut on_pending: impl FnMut() -> Result<bool, CometError>,
+) -> Result<Option<RecordBatch>, CometError>
+where
+    S: Stream<Item = DataFusionResult<RecordBatch>> + Unpin,
+{
+    loop {
+        match poll!(stream.next()) {
+            Poll::Ready(item) => return Ok(item.transpose()?),
+            Poll::Pending => {
+                // JNI call to pull batches from JVM into ScanExec operators.
+                // block_in_place lets tokio move other tasks off this worker
+                // while we wait for JVM data.
+                let pulled = tokio::task::block_in_place(&mut on_pending)?;
+                if !pulled {
+                    park_until_woken().await;
+                }
+            }
+        }
+    }
 }

Review Comment:
   Thanks, this is a better design, and I've switched to it in 398b3f94e. 
`get_next_batch` and `pull_input_batches` return `()` again, the nested 
`block_on` test's closure returns `Ok(())`, and the `development.md` paragraph 
describes the flag.
   
   One change from your sketch: when the flag is set, `next_batch` wakes the 
`block_on` waker and returns `Pending` instead of looping inside the `poll_fn`, 
so each poll starts with a fresh coop budget. A stream that has spent its 
budget wakes itself and returns `Pending`, which sets the flag. The loop only 
gets past that because `block_in_place` leaves the budget unconstrained on a 
thread that isn't a tokio worker, since its [`Reset` 
guard](https://github.com/tokio-rs/tokio/blob/75fef53d0a8590c2d1dbb63672aa7b7d1ef51155/tokio/src/runtime/scheduler/multi_thread/worker.rs#L380-L403)
 restores the budget only when there's a worker context. In a standalone tokio 
1.53.1 program with `block_in_place` taken out, so the spent budget stayed 
spent, the loop re-polled more than a million times without progress, and the 
yielding version finished in 8 polls.
   
   On `acquireMemory` inside the poll: a nested plan can't run there, because 
tokio panics with "Cannot start a runtime from within a runtime" on a 
`block_on` outside `block_in_place`. A spill that ran a Comet plan would error 
out rather than hang, so `on_pending` is the only place a nested plan can run 
today. If that changes, the flag also records a wake that arrives during the 
poll.
   
   I also moved the elapsed-time assertion into the shared test helper, because 
the timeout's timer polls the future again when it fires, and that can finish 
it. Without the check, a `WakeFlag` that didn't forward to `block_on` passed 
the native I/O test in 10 s.
   



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