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


##########
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:
   Fixed in 61f954082 by parenthesizing the parameter. The new sources have no 
other typed lambda parameters.
   



##########
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:
   Thanks, confirmed: each sort registers its two consumers when its partition 
starts, and the fair pool divides by every registered consumer, so starting all 
32 partitions at once left each consumer less than the 10 MiB merge 
reservation. Delivery now runs at most `spark.executor.cores` partitions at 
once, which `local[N]` resolves to N, and starts the next one when the first is 
delivered (61f954082), so no more partitions' operators exist at once than 
ordinary Comet runs in N tasks. Your query is now a test in the suite, which 
already runs with `local[2]` and 256 MiB off-heap, and a native test checks 
that the later partitions start only as earlier ones are delivered.
   



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