sunchao commented on code in PR #6883:
URL: https://github.com/apache/datafusion-comet/pull/6883#discussion_r4316657325


##########
native/local/src/lib.rs:
##########
@@ -0,0 +1,267 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+//! Query-scoped execution, independent of Spark tasks and JNI.
+//!
+//! A caller supplies a fresh, fully planned graph for each execution and 
receives one
+//! result stream. The caller owns the Tokio runtime and must keep it running 
until
+//! cleanup finishes. Spark admission and JNI live outside this crate.
+
+pub mod handle;
+
+use std::collections::VecDeque;
+use std::panic::AssertUnwindSafe;
+use std::pin::Pin;
+use std::sync::atomic::{AtomicBool, Ordering};
+use std::sync::{Arc, Mutex, MutexGuard};
+use std::task::{Context, Poll};
+
+use datafusion::arrow::datatypes::SchemaRef;
+use datafusion::arrow::record_batch::RecordBatch;
+use datafusion::common::{DataFusionError, Result};
+use datafusion::execution::TaskContext;
+use datafusion::physical_plan::stream::RecordBatchStreamAdapter;
+use datafusion::physical_plan::{
+    execute_stream, ExecutionPlan, ExecutionPlanProperties, RecordBatchStream,
+    SendableRecordBatchStream,
+};
+use futures::future::poll_fn;
+use futures::{FutureExt, Stream, StreamExt};
+use tokio::task::JoinHandle;
+
+/// Batches each root partition may run ahead of the one being delivered, when 
results are
+/// delivered in partition order.
+const PARTITION_BUFFER: usize = 2;
+
+/// One execution's graph and resources. Consumed when execution starts; not a 
plan cache.
+pub struct LocalQuery {
+    plan: Arc<dyn ExecutionPlan>,
+    context: Arc<TaskContext>,
+    in_partition_order: bool,
+}
+
+impl LocalQuery {
+    /// Takes ownership of a fresh graph and its query-scoped context.
+    ///
+    /// The planner must satisfy operator distribution and ordering 
requirements. It must not
+    /// reuse stateful operator instances from another query or attempt.
+    pub fn new(plan: Arc<dyn ExecutionPlan>, context: Arc<TaskContext>) -> 
Self {
+        Self {
+            plan,
+            context,
+            in_partition_order: false,
+        }
+    }
+
+    /// Delivers the root partitions one after another, in partition order, as 
Spark returns
+    /// the partitions of a result. All partitions still run concurrently, 
each up to a few
+    /// batches ahead. Only for roots whose partitions do not share an 
exchange: a partition
+    /// that waits to be delivered would otherwise leave its exchange 
buffering for the others.
+    pub fn in_partition_order(mut self) -> Self {
+        self.in_partition_order = true;
+        self
+    }
+
+    /// Starts all root partitions and exposes one streaming result.
+    ///
+    /// By default the partitions are coalesced as they produce batches, which 
does not
+    /// preserve their order. A single-partition root preserves its ordering 
either way. No
+    /// result-wide collection is performed here.
+    pub fn execute(self) -> Result<LocalQueryStream> {
+        tokio::runtime::Handle::try_current().map_err(|_| {
+            DataFusionError::Execution("LocalQuery requires an active Tokio 
runtime".into())
+        })?;
+        let schema = self.plan.schema();
+        let stream = if self.in_partition_order {
+            in_partition_order(&self.plan, &self.context)?
+        } else {
+            execute_stream(Arc::clone(&self.plan), Arc::clone(&self.context))?
+        };
+        Ok(LocalQueryStream {
+            stream: Some(stream),
+            query: Some(self),
+            schema,
+        })
+    }
+}
+
+fn in_partition_order(
+    plan: &Arc<dyn ExecutionPlan>,
+    context: &Arc<TaskContext>,
+) -> Result<SendableRecordBatchStream> {
+    let runtime = tokio::runtime::Handle::current();
+    let partitions = (0..plan.output_partitioning().partition_count())
+        .map(|partition| {
+            let stream = plan.execute(partition, Arc::clone(context))?;

Review Comment:
   [P2] Bound active partitions before initializing their memory consumers. 
With `local[2]`, 256 MiB off-heap memory, the default `fair_unified` pool and 
local/range execution enabled, `spark.range(0, 32000, 1, 32).selectExpr("id", 
"sha2(cast(id as string), 256) AS k").sortWithinPartitions("k").collect()` 
fails with `Not enough memory to continue external sort`. Spark and ordinary 
Comet return all 32,000 rows. Calling `execute` for every partition registers 
every sort and merge consumer in the same pool, shrinking their fair shares 
below the sort’s 10 MiB minimum reservation. The two-batch queue does not 
prevent this. Initialize only a bounded number of active partitions, or fall 
back for these plans until their reservation requirements can be supported.
   
   Evidence: Reproduced on Spark 4.1.3/JDK 21 using current-head JVM sources 
and checksum-verified, source-identical native code. Spark and ordinary Comet 
returned rows=32000, sum=511984000. Local execution failed acquiring 10485760 
bytes with a fair limit of 7064090 bytes and 38 registered consumers. The same 
query with two partitions succeeded in all three modes. DataFusion 55.2.0 
`ExternalSorter::new` registers two consumers per sort, and 
`reserve_memory_for_merge` requires the reservation before processing input. 
Probe and output: `/tmp/comet6883-independent-oiwwar3j/ReviewMemoryProbe.scala` 
and `memory.log`.



##########
spark/src/test/scala/org/apache/comet/local/CometLocalExecutionSuite.scala:
##########
@@ -0,0 +1,503 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.comet.local
+
+import java.util.concurrent.{Callable, Executors, TimeUnit}
+
+import scala.jdk.CollectionConverters._
+import scala.reflect.ClassTag
+
+import org.scalatest.{BeforeAndAfterAll, BeforeAndAfterEach}
+import org.scalatest.funsuite.AnyFunSuite
+
+import org.apache.spark.{SparkEnv, SparkException, TaskContext}
+import org.apache.spark.sql.{DataFrame, Row, SparkSession}
+import org.apache.spark.sql.execution.columnar.InMemoryRelation
+import org.apache.spark.sql.execution.exchange.ShuffleExchangeLike
+import org.apache.spark.sql.expressions.Window
+import org.apache.spark.sql.functions.{col, desc, row_number}
+
+import org.apache.comet.{CometArrowImportAllocator, CometConf, 
CometExecIterator, CometSparkSessionExtensions, NativeBase}
+import org.apache.comet.vector.NativeUtil
+
+class CometLocalExecutionSuite
+    extends AnyFunSuite
+    with BeforeAndAfterAll
+    with BeforeAndAfterEach {
+  private var spark: SparkSession = _
+  private val enabled = CometConf.COMET_EXEC_LOCAL_ENABLED.key
+
+  override protected def beforeAll(): Unit = {
+    super.beforeAll()
+    spark = SparkSession
+      .builder()
+      .master("local[2]")
+      .appName("CometLocalExecutionSuite")
+      .config("spark.ui.enabled", "false")
+      .config("spark.sql.session.timeZone", "UTC")
+      .config("spark.sql.extensions", 
classOf[CometSparkSessionExtensions].getName)
+      // AQE stays at Spark's default: it does not wrap the exchange-free 
plans admitted here.
+      .config("spark.memory.offHeap.enabled", "true")
+      .config("spark.memory.offHeap.size", "256m")
+      .config("spark.comet.enabled", "true")
+      .config("spark.comet.exec.enabled", "true")
+      .config("spark.comet.shuffle.enabled", "false")
+      .config(CometConf.COMET_EXEC_RANGE_ENABLED.key, "true")
+      .config("spark.comet.batchSize", "17")
+      .config(enabled, "true")
+      .getOrCreate()
+    spark.sparkContext.setLogLevel("WARN")
+    assert(NativeBase.isLoaded)
+  }
+
+  override protected def afterEach(): Unit = {
+    try {
+      assert(new NativeLocal().activeQueries() == 0)
+      assert(CometArrowImportAllocator.getAllocatedMemory == 0)
+      assert(LocalResultHandoff.active == 0)
+    } finally super.afterEach()
+  }
+
+  override protected def afterAll(): Unit = {
+    try {
+      if (spark != null) spark.stop()
+      SparkSession.clearActiveSession()
+      SparkSession.clearDefaultSession()
+    } finally super.afterAll()
+  }
+
+  private def withConf[T](key: String, value: String)(f: => T): T = {
+    val previous = spark.conf.getOption(key)
+    spark.conf.set(key, value)
+    try f
+    finally
+      previous match {
+        case Some(v) => spark.conf.set(key, v)
+        case None => spark.conf.unset(key)
+      }
+  }
+
+  private def localNodes(df: DataFrame): Seq[CometLocalExec] =
+    df.queryExecution.executedPlan.collect { case p: CometLocalExec => p }
+
+  /** Spark's own answer, with Comet disabled. */
+  private def expected(query: => DataFrame): Seq[Row] =
+    withConf("spark.comet.enabled", "false")(query.collect().toSeq)
+
+  /** Checks that `query` runs locally and returns Spark's rows in Spark's 
order. */
+  private def checkLocal(query: => DataFrame): Unit = {
+    val actual = query
+    assert(localNodes(actual).size == 1, 
actual.queryExecution.executedPlan.toString)
+    assert(actual.collect().toSeq == expected(query))
+  }
+
+  /** Runs `f` in a Spark task, as the local query iterator does. */
+  private def inTask[T: ClassTag](f: TaskContext => T): T =
+    spark.sparkContext
+      .runJob(
+        spark.sparkContext.parallelize(Seq(0), 1),
+        { _: Iterator[Int] =>

Review Comment:
   [P2] Use callback syntax accepted by the required syntactic parser. Now that 
this suite lives under `spark/`, CI’s `scalafix:0.14.6 --check --syntactic` 
command includes it and rejects `{ _: Iterator[Int] =>` with `` `}` expected 
but `=>` found ``. This fails the required Scala lint job even though Scala 
compilation succeeds. Parenthesize the typed parameter, for example `{ (_: 
Iterator[Int]) => ... }`, so the suite passes the configured parser.
   
   Evidence: Exact-head check `PR Build (Linux) / Lint Scala (syntactic)` 
failed at `CometLocalExecutionSuite.scala:116:28` and exited 2: 
https://github.com/apache/datafusion-comet/actions/runs/39289196110/job/118188258091.
 Retrieved the job log and verified its check-run head SHA is 
`b82f3878384044d6607c9c4fdf6cf09c77ddcbcd`. Saved log: 
`/tmp/comet6883-independent-oiwwar3j/scala-lint.log`.



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