Copilot commented on code in PR #7570:
URL: https://github.com/apache/kyuubi/pull/7570#discussion_r3594015781


##########
extensions/spark/kyuubi-extension-spark-4-2/src/main/scala/org/apache/spark/sql/FinalStageResourceManager.scala:
##########
@@ -0,0 +1,289 @@
+/*
+ * 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.spark.sql
+
+import scala.annotation.tailrec
+import scala.collection.mutable
+import scala.collection.mutable.ArrayBuffer
+
+import org.apache.spark.{ExecutorAllocationClient, MapOutputTrackerMaster, 
SparkContext, SparkEnv}
+import org.apache.spark.internal.Logging
+import org.apache.spark.resource.ResourceProfile
+import org.apache.spark.scheduler.cluster.CoarseGrainedSchedulerBackend
+import org.apache.spark.sql.catalyst.rules.Rule
+import org.apache.spark.sql.execution.{FilterExec, ProjectExec, SortExec, 
SparkPlan}
+import org.apache.spark.sql.execution.adaptive._
+import org.apache.spark.sql.execution.columnar.InMemoryTableScanExec
+import org.apache.spark.sql.execution.command.DataWritingCommandExec
+import org.apache.spark.sql.execution.datasources.WriteFilesExec
+import org.apache.spark.sql.execution.datasources.v2.V2TableWriteExec
+import org.apache.spark.sql.execution.exchange.{ENSURE_REQUIREMENTS, 
ShuffleExchangeExec}
+
+import org.apache.kyuubi.sql.{KyuubiSQLConf, WriteUtils}
+
+/**
+ * This rule assumes the final write stage has less cores requirement than 
previous, otherwise
+ * this rule would take no effect.
+ *
+ * It provide a feature:
+ * 1. Kill redundant executors before running final write stage
+ */
+case class FinalStageResourceManager(session: SparkSession)
+  extends Rule[SparkPlan] with FinalRebalanceStageHelper {
+  override def apply(plan: SparkPlan): SparkPlan = {
+    if 
(!conf.getConf(KyuubiSQLConf.FINAL_WRITE_STAGE_EAGERLY_KILL_EXECUTORS_ENABLED)) 
{
+      return plan
+    }
+
+    if (!WriteUtils.isWrite(session, plan)) {
+      return plan
+    }
+
+    val sc = session.sparkContext
+    val dra = sc.getConf.getBoolean("spark.dynamicAllocation.enabled", false)
+    val coresPerExecutor = sc.getConf.getInt("spark.executor.cores", 1)
+    val minExecutors = 
sc.getConf.getInt("spark.dynamicAllocation.minExecutors", 0)
+    val maxExecutors = 
sc.getConf.getInt("spark.dynamicAllocation.maxExecutors", Int.MaxValue)
+    val factor = conf.getConf(KyuubiSQLConf.FINAL_WRITE_STAGE_PARTITION_FACTOR)
+    val hasImprovementRoom = maxExecutors - 1 > minExecutors * factor
+    // Fast fail if:
+    // 1. DRA off
+    // 2. only work with yarn and k8s
+    // 3. maxExecutors is not bigger than minExecutors * factor
+    if (!dra || 
!sc.schedulerBackend.isInstanceOf[CoarseGrainedSchedulerBackend] ||
+      !hasImprovementRoom) {
+      return plan
+    }
+
+    val stageOpt = findFinalRebalanceStage(plan)
+    if (stageOpt.isEmpty) {
+      return plan
+    }
+
+    // It's not safe to kill executors if this plan contains table cache.
+    // If the executor loses then the rdd would re-compute those partition.
+    if (hasTableCache(plan) &&
+      
conf.getConf(KyuubiSQLConf.FINAL_WRITE_STAGE_SKIP_KILLING_EXECUTORS_FOR_TABLE_CACHE))
 {
+      return plan
+    }
+
+    // TODO: move this to query stage optimizer when updating Spark to 3.5.x
+    // Since we are in `prepareQueryStage`, the AQE shuffle read has not been 
applied.
+    // So we need to apply it by self.
+    val shuffleRead = 
queryStageOptimizerRules.foldLeft(stageOpt.get.asInstanceOf[SparkPlan]) {
+      case (latest, rule) => rule.apply(latest)
+    }
+    val (targetCores, stage) = shuffleRead match {
+      case AQEShuffleReadExec(stage: ShuffleQueryStageExec, partitionSpecs) =>
+        (partitionSpecs.length, stage)
+      case stage: ShuffleQueryStageExec =>
+        // we can still kill executors if no AQE shuffle read, e.g., 
`.repartition(2)`
+        (stage.shuffle.numPartitions, stage)
+      case _ =>
+        // it should never happen in current Spark, but to be safe do nothing 
if happens
+        logWarning("BUG, Please report to Apache Kyuubi community")
+        return plan
+    }
+    // The condition whether inject custom resource profile:
+    // - target executors < active executors
+    // - active executors - target executors > min executors
+    val numActiveExecutors = sc.getExecutorIds().length
+    val targetExecutors = (math.ceil(targetCores.toFloat / coresPerExecutor) * 
factor).toInt
+      .max(1)
+    val hasBenefits = targetExecutors < numActiveExecutors &&
+      (numActiveExecutors - targetExecutors) > minExecutors
+    logInfo(s"The snapshot of current executors view, " +
+      s"active executors: $numActiveExecutors, min executor: $minExecutors, " +
+      s"target executors: $targetExecutors, has benefits: $hasBenefits")
+    if (hasBenefits) {
+      val shuffleId = 
stage.plan.asInstanceOf[ShuffleExchangeExec].shuffleDependency.shuffleId
+      val numReduce = 
stage.plan.asInstanceOf[ShuffleExchangeExec].numPartitions
+      // Now, there is only a final rebalance stage waiting to execute and all 
tasks of previous
+      // stage are finished. Kill redundant existed executors eagerly so the 
tasks of final
+      // stage can be centralized scheduled.
+      killExecutors(sc, targetExecutors, shuffleId, numReduce)
+    }
+
+    plan
+  }
+
+  /**
+   * The priority of kill executors follow:
+   * 1. kill executor who is younger than other (The older the JIT works 
better)
+   * 2. kill executor who produces less shuffle data first
+   */
+  private def findExecutorToKill(
+      sc: SparkContext,
+      targetExecutors: Int,
+      shuffleId: Int,
+      numReduce: Int): Seq[String] = {
+    val tracker = 
SparkEnv.get.mapOutputTracker.asInstanceOf[MapOutputTrackerMaster]
+    val shuffleStatusOpt = tracker.shuffleStatuses.get(shuffleId)
+    if (shuffleStatusOpt.isEmpty) {
+      return Seq.empty
+    }
+    val shuffleStatus = shuffleStatusOpt.get
+    val executorToBlockSize = new mutable.HashMap[String, Long]
+    shuffleStatus.withMapStatuses { mapStatus =>
+      mapStatus.foreach { status =>
+        var i = 0
+        var sum = 0L
+        while (i < numReduce) {
+          sum += status.getSizeForBlock(i)
+          i += 1
+        }
+        executorToBlockSize.getOrElseUpdate(status.location.executorId, sum)

Review Comment:
   `executorToBlockSize.getOrElseUpdate(...)` only records the first map status 
per executor. Since there are multiple map tasks per executor, this undercounts 
shuffle block sizes and can pick the wrong executors to kill.



##########
extensions/spark/kyuubi-extension-spark-4-2/src/main/scala/org/apache/spark/sql/execution/CustomResourceProfileExec.scala:
##########
@@ -0,0 +1,117 @@
+/*
+ * 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.spark.sql.execution
+
+import org.apache.spark.network.util.{ByteUnit, JavaUtils}
+import org.apache.spark.rdd.RDD
+import org.apache.spark.resource.{ExecutorResourceRequests, 
ResourceProfileBuilder}
+import org.apache.spark.sql.catalyst.InternalRow
+import org.apache.spark.sql.catalyst.expressions.{Attribute, SortOrder}
+import org.apache.spark.sql.catalyst.plans.physical.Partitioning
+import org.apache.spark.sql.execution.metric.{SQLMetric, SQLMetrics}
+import org.apache.spark.sql.vectorized.ColumnarBatch
+import org.apache.spark.util.Utils
+
+import org.apache.kyuubi.sql.KyuubiSQLConf._
+
+/**
+ * This node wraps the final executed plan and inject custom resource profile 
to the RDD.
+ * It assumes that, the produced RDD would create the `ResultStage` in 
`DAGScheduler`,
+ * so it makes resource isolation between previous and final stage.
+ *
+ * Note that, Spark does not support config `minExecutors` for each resource 
profile.
+ * Which means, it would retain `minExecutors` for each resource profile.
+ * So, suggest set `spark.dynamicAllocation.minExecutors` to 0 if enable this 
feature.
+ */
+case class CustomResourceProfileExec(child: SparkPlan) extends UnaryExecNode {
+  override def output: Seq[Attribute] = child.output
+  override def outputPartitioning: Partitioning = child.outputPartitioning
+  override def outputOrdering: Seq[SortOrder] = child.outputOrdering
+  override def supportsColumnar: Boolean = child.supportsColumnar
+  override def supportsRowBased: Boolean = child.supportsRowBased
+  override protected def doCanonicalize(): SparkPlan = child.canonicalized
+
+  private val executorCores = 
conf.getConf(FINAL_WRITE_STAGE_EXECUTOR_CORES).getOrElse(
+    sparkContext.getConf.getInt("spark.executor.cores", 1))
+  private val executorMemory = 
conf.getConf(FINAL_WRITE_STAGE_EXECUTOR_MEMORY).getOrElse(
+    sparkContext.getConf.get("spark.executor.memory", "2G"))
+  private val executorMemoryOverhead =
+    conf.getConf(FINAL_WRITE_STAGE_EXECUTOR_MEMORY_OVERHEAD)
+      .getOrElse(sparkContext.getConf.get("spark.executor.memoryOverhead", 
"1G"))
+  private val executorOffHeapMemory =
+    if (sparkContext.getConf.getBoolean("spark.memory.offHeap.enabled", 
false)) {
+      conf.getConf(FINAL_WRITE_STAGE_EXECUTOR_OFF_HEAP_MEMORY)
+    } else {
+      None
+    }
+
+  override lazy val metrics: Map[String, SQLMetric] = {
+    val base = Map(
+      "executorCores" -> SQLMetrics.createMetric(sparkContext, "executor 
cores"),
+      "executorMemory" -> SQLMetrics.createMetric(sparkContext, "executor 
memory (MiB)"),
+      "executorMemoryOverhead" -> SQLMetrics.createMetric(
+        sparkContext,
+        "executor memory overhead (MiB)"))
+    val addition = executorOffHeapMemory.map(_ =>
+      "executorOffHeapMemory" ->
+        SQLMetrics.createMetric(sparkContext, "executor off heap memory 
(MiB)")).toMap
+    base ++ addition
+  }
+
+  private def wrapResourceProfile[T](rdd: RDD[T]): RDD[T] = {
+    if (Utils.isTesting) {
+      // do nothing for local testing
+      return rdd
+    }
+
+    metrics("executorCores") += executorCores
+    metrics("executorMemory") += JavaUtils.byteStringAs(executorMemory, 
ByteUnit.MiB)
+    metrics("executorMemoryOverhead") += JavaUtils.byteStringAs(
+      executorMemoryOverhead,
+      ByteUnit.MiB)
+    executorOffHeapMemory.foreach(m =>
+      metrics("executorOffHeapMemory") += JavaUtils.byteStringAs(m, 
ByteUnit.MiB))
+
+    val executionId = 
sparkContext.getLocalProperty(SQLExecution.EXECUTION_ID_KEY)
+    SQLMetrics.postDriverMetricUpdates(sparkContext, executionId, 
metrics.values.toSeq)
+
+    val resourceProfileBuilder = new ResourceProfileBuilder()
+    val executorResourceRequests = new ExecutorResourceRequests()
+    executorResourceRequests.cores(executorCores)
+    executorResourceRequests.memory(executorMemory)
+    executorResourceRequests.memoryOverhead(executorMemoryOverhead)
+    executorOffHeapMemory.foreach(executorResourceRequests.offHeapMemory)
+    resourceProfileBuilder.require(executorResourceRequests)
+    rdd.withResources(resourceProfileBuilder.build())
+    rdd

Review Comment:
   `wrapResourceProfile` calls `rdd.withResources(...)` but discards the 
returned RDD, so the custom ResourceProfile is never applied (only metrics are 
updated). This makes final-stage resource isolation ineffective.



##########
pom.xml:
##########
@@ -2110,6 +2110,34 @@
             </properties>
         </profile>
 
+        <profile>
+            <id>spark-4.2</id>
+            <modules>
+                <module>extensions/spark/kyuubi-extension-spark-4-2</module>
+                <module>extensions/spark/kyuubi-spark-connector-hive</module>
+            </modules>
+            <properties>
+                <maven.compiler.release>17</maven.compiler.release>
+                <enforcer.maxJdkVersion>17</enforcer.maxJdkVersion>
+                <spark.version>4.2.0</spark.version>
+                <!-- TODO: update once Delta and Iceberg support Spark 4.1. -->

Review Comment:
   This Spark 4.2 profile intentionally uses `spark.binary.version` 4.1 for 
Delta/Iceberg fallback, but the TODO comment says "support Spark 4.1" which is 
confusing in a 4.2 profile. Consider updating the comment to refer to Spark 4.2 
artifact support.



##########
extensions/spark/kyuubi-extension-spark-4-2/src/test/scala/org/apache/spark/sql/ZorderCoreBenchmark.scala:
##########
@@ -0,0 +1,117 @@
+/*
+ * 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.spark.sql
+
+import org.apache.spark.SparkConf
+import org.apache.spark.benchmark.Benchmark
+import org.apache.spark.sql.benchmark.KyuubiBenchmarkBase
+import org.apache.spark.sql.internal.StaticSQLConf
+
+import org.apache.kyuubi.sql.zorder.ZorderBytesUtils
+
+/**
+ * Benchmark to measure performance with zorder core.
+ *
+ * {{{
+ *   RUN_BENCHMARK=1 ./build/mvn clean test \
+ *   -pl extensions/spark/kyuubi-extension-spark-4-1 -am \
+ *   -Pspark-4.1,kyuubi-extension-spark-4-1,scala-2.13 \
+ *   -Dtest=none -DwildcardSuites=org.apache.spark.sql.ZorderCoreBenchmark

Review Comment:
   The benchmark scaladoc still references the Spark 4.1 module/profile 
(`kyuubi-extension-spark-4-1`, `-Pspark-4.1`). Since this file lives under the 
Spark 4.2 extension, the example command should point to the 4.2 module/profile.



##########
extensions/spark/kyuubi-extension-spark-4-2/src/main/scala/org/apache/kyuubi/sql/KyuubiSQLConf.scala:
##########
@@ -0,0 +1,332 @@
+/*
+ * 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.kyuubi.sql
+
+import org.apache.spark.network.util.{ByteUnit, JavaUtils}
+import org.apache.spark.sql.internal.SQLConf
+import org.apache.spark.sql.internal.SQLConf._
+
+object KyuubiSQLConf {
+
+  final val FINAL_STAGE_ADVISORY_PARTITION_SIZE_KEY =
+    "spark.sql.finalStage.adaptive.advisoryPartitionSizeInBytes"
+
+  val INSERT_REPARTITION_BEFORE_WRITE =
+    buildConf("spark.sql.optimizer.insertRepartitionBeforeWrite.enabled")
+      .doc("Add repartition node at the top of query plan. An approach of 
merging small files.")
+      .version("1.2.0")
+      .booleanConf
+      .createWithDefault(true)
+
+  val FORCE_SHUFFLE_BEFORE_JOIN =
+    buildConf("spark.sql.optimizer.forceShuffleBeforeJoin.enabled")
+      .doc("Ensure shuffle node exists before shuffled join (shj and smj) to 
make AQE " +
+        "`OptimizeSkewedJoin` works (complex scenario join, multi table 
join).")
+      .version("1.2.0")
+      .booleanConf
+      .createWithDefault(false)
+
+  val FINAL_STAGE_CONFIG_ISOLATION =
+    buildConf("spark.sql.optimizer.finalStageConfigIsolation.enabled")
+      .doc("If true, the final stage support use different config with 
previous stage. " +
+        "The prefix of final stage config key should be 
`spark.sql.finalStage.`." +
+        "For example, the raw spark config: 
`spark.sql.adaptive.advisoryPartitionSizeInBytes`, " +
+        "then the final stage config should be: " +
+        "`spark.sql.finalStage.adaptive.advisoryPartitionSizeInBytes`.")
+      .version("1.2.0")
+      .booleanConf
+      .createWithDefault(false)
+
+  val INSERT_ZORDER_BEFORE_WRITING =
+    buildConf("spark.sql.optimizer.insertZorderBeforeWriting.enabled")
+      .doc("When true, we will follow target table properties to insert zorder 
or not. " +
+        "The key properties are: 1) kyuubi.zorder.enabled; if this property is 
true, we will " +
+        "insert zorder before writing data. 2) kyuubi.zorder.cols; string 
split by comma, we " +
+        "will zorder by these cols.")
+      .version("1.4.0")
+      .booleanConf
+      .createWithDefault(true)
+
+  val ZORDER_GLOBAL_SORT_ENABLED =
+    buildConf("spark.sql.optimizer.zorderGlobalSort.enabled")
+      .doc("When true, we do a global sort using zorder. Note that, it can 
cause data skew " +
+        "issue if the zorder columns have less cardinality. When false, we 
only do local sort " +
+        "using zorder.")
+      .version("1.4.0")
+      .booleanConf
+      .createWithDefault(true)
+
+  val REBALANCE_BEFORE_ZORDER =
+    buildConf("spark.sql.optimizer.rebalanceBeforeZorder.enabled")
+      .doc("When true, we do a rebalance before zorder in case data skew. " +
+        "Note that, if the insertion is dynamic partition we will use the 
partition " +
+        "columns to rebalance.")
+      .version("1.6.0")
+      .booleanConf
+      .createWithDefault(false)
+
+  val REBALANCE_ZORDER_COLUMNS_ENABLED =
+    buildConf("spark.sql.optimizer.rebalanceZorderColumns.enabled")
+      .doc(s"When true and ${REBALANCE_BEFORE_ZORDER.key} is true, we do 
rebalance before " +
+        s"Z-Order. If it's dynamic partition insert, the rebalance expression 
will include " +
+        s"both partition columns and Z-Order columns.")
+      .version("1.6.0")
+      .booleanConf
+      .createWithDefault(false)
+
+  val TWO_PHASE_REBALANCE_BEFORE_ZORDER =
+    buildConf("spark.sql.optimizer.twoPhaseRebalanceBeforeZorder.enabled")
+      .doc(s"When true and ${REBALANCE_BEFORE_ZORDER.key} is true, we do two 
phase rebalance " +
+        s"before Z-Order for the dynamic partition write. The first phase 
rebalance using " +
+        s"dynamic partition column; The second phase rebalance using dynamic 
partition column + " +
+        s"Z-Order columns.")
+      .version("1.6.0")
+      .booleanConf
+      .createWithDefault(false)
+
+  val ZORDER_USING_ORIGINAL_ORDERING_ENABLED =
+    buildConf("spark.sql.optimizer.zorderUsingOriginalOrdering.enabled")
+      .doc(s"When true and ${REBALANCE_BEFORE_ZORDER.key} is true, we do sort 
by " +
+        s"the original ordering i.e. lexicographical order.")
+      .version("1.6.0")
+      .booleanConf
+      .createWithDefault(false)
+
+  val WATCHDOG_MAX_PARTITIONS =
+    buildConf("spark.sql.watchdog.maxPartitions")
+      .doc("Set the max partition number when spark scans a data source. " +
+        "Enable maxPartitions Strategy by specifying this configuration. " +
+        "Add maxPartitions Strategy to avoid scan excessive partitions " +
+        "on partitioned table, it's optional that works with defined")
+      .version("1.4.0")
+      .intConf
+      .createOptional
+
+  val WATCHDOG_MAX_FILE_SIZE =
+    buildConf("spark.sql.watchdog.maxFileSize")
+      .doc("Set the maximum size in bytes of files when spark scans a data 
source. " +
+        "Enable maxFileSize Strategy by specifying this configuration. " +
+        "Add maxFileSize Strategy to avoid scan excessive size of files," +
+        " it's optional that works with defined")
+      .version("1.8.0")
+      .bytesConf(ByteUnit.BYTE)
+      .createOptional
+
+  val DROP_IGNORE_NONEXISTENT =
+    buildConf("spark.sql.optimizer.dropIgnoreNonExistent")
+      .doc("Do not report an error if DROP 
DATABASE/TABLE/VIEW/FUNCTION/PARTITION specifies " +
+        "a non-existent database/table/view/function/partition")
+      .version("1.5.0")
+      .booleanConf
+      .createWithDefault(false)
+
+  val INFER_REBALANCE_AND_SORT_ORDERS =
+    buildConf("spark.sql.optimizer.inferRebalanceAndSortOrders.enabled")
+      .doc("When ture, infer columns for rebalance and sort orders from 
original query, " +

Review Comment:
   Typo in config documentation: "When ture" -> "When true".



##########
pom.xml:
##########
@@ -2110,6 +2110,34 @@
             </properties>
         </profile>
 
+        <profile>
+            <id>spark-4.2</id>
+            <modules>
+                <module>extensions/spark/kyuubi-extension-spark-4-2</module>
+                <module>extensions/spark/kyuubi-spark-connector-hive</module>
+            </modules>
+            <properties>
+                <maven.compiler.release>17</maven.compiler.release>
+                <enforcer.maxJdkVersion>17</enforcer.maxJdkVersion>
+                <spark.version>4.2.0</spark.version>
+                <!-- TODO: update once Delta and Iceberg support Spark 4.1. -->
+                <spark.binary.version>4.1</spark.binary.version>
+                <!-- SPARK-47086 (4.2.0) upgraded Jetty to 12 (ee10), which 
requires the
+                     Jakarta Servlet 6.0 API; jakarta.servlet.ServletConnection
+                     was introduced in Servlet 6.0. -->
+                
<jakarta.servlet-api.version>6.0.0</jakarta.servlet-api.version>
+                <antlr4.version>4.13.1</antlr4.version>
+                <delta.version>4.3.1</delta.version>
+                
<delta.artifact>delta-spark_${spark.binary.version}_${scala.binary.version}</delta.artifact>
+                <hudi.version>1.2.0</hudi.version>
+                <!-- TODO: update once Paimon support Spark 4.1. -->

Review Comment:
   Similar to the Delta/Iceberg TODO above: this comment mentions Spark 4.1 in 
the Spark 4.2 profile, but the fallback artifact is `paimon-spark-4.0_*`. 
Update the TODO to reference Spark 4.2 support to avoid confusion.



##########
extensions/spark/kyuubi-extension-spark-4-2/src/main/scala/org/apache/spark/sql/FinalStageResourceManager.scala:
##########
@@ -0,0 +1,289 @@
+/*
+ * 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.spark.sql
+
+import scala.annotation.tailrec
+import scala.collection.mutable
+import scala.collection.mutable.ArrayBuffer
+
+import org.apache.spark.{ExecutorAllocationClient, MapOutputTrackerMaster, 
SparkContext, SparkEnv}
+import org.apache.spark.internal.Logging
+import org.apache.spark.resource.ResourceProfile
+import org.apache.spark.scheduler.cluster.CoarseGrainedSchedulerBackend
+import org.apache.spark.sql.catalyst.rules.Rule
+import org.apache.spark.sql.execution.{FilterExec, ProjectExec, SortExec, 
SparkPlan}
+import org.apache.spark.sql.execution.adaptive._
+import org.apache.spark.sql.execution.columnar.InMemoryTableScanExec
+import org.apache.spark.sql.execution.command.DataWritingCommandExec
+import org.apache.spark.sql.execution.datasources.WriteFilesExec
+import org.apache.spark.sql.execution.datasources.v2.V2TableWriteExec
+import org.apache.spark.sql.execution.exchange.{ENSURE_REQUIREMENTS, 
ShuffleExchangeExec}
+
+import org.apache.kyuubi.sql.{KyuubiSQLConf, WriteUtils}
+
+/**
+ * This rule assumes the final write stage has less cores requirement than 
previous, otherwise
+ * this rule would take no effect.
+ *
+ * It provide a feature:
+ * 1. Kill redundant executors before running final write stage
+ */
+case class FinalStageResourceManager(session: SparkSession)
+  extends Rule[SparkPlan] with FinalRebalanceStageHelper {
+  override def apply(plan: SparkPlan): SparkPlan = {
+    if 
(!conf.getConf(KyuubiSQLConf.FINAL_WRITE_STAGE_EAGERLY_KILL_EXECUTORS_ENABLED)) 
{
+      return plan
+    }
+
+    if (!WriteUtils.isWrite(session, plan)) {
+      return plan
+    }
+
+    val sc = session.sparkContext
+    val dra = sc.getConf.getBoolean("spark.dynamicAllocation.enabled", false)
+    val coresPerExecutor = sc.getConf.getInt("spark.executor.cores", 1)
+    val minExecutors = 
sc.getConf.getInt("spark.dynamicAllocation.minExecutors", 0)
+    val maxExecutors = 
sc.getConf.getInt("spark.dynamicAllocation.maxExecutors", Int.MaxValue)
+    val factor = conf.getConf(KyuubiSQLConf.FINAL_WRITE_STAGE_PARTITION_FACTOR)
+    val hasImprovementRoom = maxExecutors - 1 > minExecutors * factor
+    // Fast fail if:
+    // 1. DRA off
+    // 2. only work with yarn and k8s
+    // 3. maxExecutors is not bigger than minExecutors * factor
+    if (!dra || 
!sc.schedulerBackend.isInstanceOf[CoarseGrainedSchedulerBackend] ||
+      !hasImprovementRoom) {
+      return plan
+    }
+
+    val stageOpt = findFinalRebalanceStage(plan)
+    if (stageOpt.isEmpty) {
+      return plan
+    }
+
+    // It's not safe to kill executors if this plan contains table cache.
+    // If the executor loses then the rdd would re-compute those partition.
+    if (hasTableCache(plan) &&
+      
conf.getConf(KyuubiSQLConf.FINAL_WRITE_STAGE_SKIP_KILLING_EXECUTORS_FOR_TABLE_CACHE))
 {
+      return plan
+    }
+
+    // TODO: move this to query stage optimizer when updating Spark to 3.5.x
+    // Since we are in `prepareQueryStage`, the AQE shuffle read has not been 
applied.
+    // So we need to apply it by self.
+    val shuffleRead = 
queryStageOptimizerRules.foldLeft(stageOpt.get.asInstanceOf[SparkPlan]) {
+      case (latest, rule) => rule.apply(latest)
+    }
+    val (targetCores, stage) = shuffleRead match {
+      case AQEShuffleReadExec(stage: ShuffleQueryStageExec, partitionSpecs) =>
+        (partitionSpecs.length, stage)
+      case stage: ShuffleQueryStageExec =>
+        // we can still kill executors if no AQE shuffle read, e.g., 
`.repartition(2)`
+        (stage.shuffle.numPartitions, stage)
+      case _ =>
+        // it should never happen in current Spark, but to be safe do nothing 
if happens
+        logWarning("BUG, Please report to Apache Kyuubi community")
+        return plan
+    }
+    // The condition whether inject custom resource profile:
+    // - target executors < active executors
+    // - active executors - target executors > min executors
+    val numActiveExecutors = sc.getExecutorIds().length
+    val targetExecutors = (math.ceil(targetCores.toFloat / coresPerExecutor) * 
factor).toInt
+      .max(1)
+    val hasBenefits = targetExecutors < numActiveExecutors &&
+      (numActiveExecutors - targetExecutors) > minExecutors
+    logInfo(s"The snapshot of current executors view, " +
+      s"active executors: $numActiveExecutors, min executor: $minExecutors, " +
+      s"target executors: $targetExecutors, has benefits: $hasBenefits")
+    if (hasBenefits) {
+      val shuffleId = 
stage.plan.asInstanceOf[ShuffleExchangeExec].shuffleDependency.shuffleId
+      val numReduce = 
stage.plan.asInstanceOf[ShuffleExchangeExec].numPartitions
+      // Now, there is only a final rebalance stage waiting to execute and all 
tasks of previous
+      // stage are finished. Kill redundant existed executors eagerly so the 
tasks of final
+      // stage can be centralized scheduled.
+      killExecutors(sc, targetExecutors, shuffleId, numReduce)
+    }
+
+    plan
+  }
+
+  /**
+   * The priority of kill executors follow:
+   * 1. kill executor who is younger than other (The older the JIT works 
better)
+   * 2. kill executor who produces less shuffle data first
+   */
+  private def findExecutorToKill(
+      sc: SparkContext,
+      targetExecutors: Int,
+      shuffleId: Int,
+      numReduce: Int): Seq[String] = {
+    val tracker = 
SparkEnv.get.mapOutputTracker.asInstanceOf[MapOutputTrackerMaster]
+    val shuffleStatusOpt = tracker.shuffleStatuses.get(shuffleId)
+    if (shuffleStatusOpt.isEmpty) {
+      return Seq.empty
+    }
+    val shuffleStatus = shuffleStatusOpt.get
+    val executorToBlockSize = new mutable.HashMap[String, Long]
+    shuffleStatus.withMapStatuses { mapStatus =>
+      mapStatus.foreach { status =>
+        var i = 0
+        var sum = 0L
+        while (i < numReduce) {
+          sum += status.getSizeForBlock(i)
+          i += 1
+        }
+        executorToBlockSize.getOrElseUpdate(status.location.executorId, sum)
+      }
+    }
+
+    val backend = 
sc.schedulerBackend.asInstanceOf[CoarseGrainedSchedulerBackend]
+    val executorsWithRegistrationTs = backend.getExecutorsWithRegistrationTs()
+    val existedExecutors = executorsWithRegistrationTs.keys.toSet
+    val expectedNumExecutorToKill = existedExecutors.size - targetExecutors
+    if (expectedNumExecutorToKill < 1) {
+      return Seq.empty
+    }
+
+    val executorIdsToKill = new ArrayBuffer[String]()
+    // We first kill executor who does not hold shuffle block. It would happen 
because
+    // the last stage is running fast and finished in a short time. The 
existed executors are
+    // from previous stages that have not been killed by DRA, so we can not 
find it by tracking
+    // shuffle status.
+    // We should evict executors by their alive time first and retain all of 
executors which
+    // have better locality for shuffle block.
+    executorsWithRegistrationTs.toSeq.sortBy(_._2).foreach { case (id, _) =>

Review Comment:
   The comment says to kill younger executors first, but the current sort order 
(`sortBy(_._2)`) iterates older executors first (smaller registration 
timestamps). Either reverse the ordering or adjust the comment to match the 
intended behavior.



##########
.github/workflows/master.yml:
##########
@@ -98,6 +104,12 @@ jobs:
             spark-archive: '-Pscala-2.13 
-Dspark.archive.mirror=https://www.apache.org/dyn/closer.lua/spark/spark-4.1.2 
-Dspark.archive.name=spark-4.1.2-bin-hadoop3.tgz'
             exclude-tags: 
'-Dmaven.plugin.scalatest.exclude.tags=org.scalatest.tags.Slow,org.apache.kyuubi.tags.DeltaTest,org.apache.kyuubi.tags.IcebergTest,org.apache.kyuubi.tags.PaimonTest,org.apache.kyuubi.tags.HudiTest,org.apache.kyuubi.tags.SparkLocalClusterTest'
             comment: 'verify-on-spark-4.1-binary'
+          - java: 17
+            python: '3.11'
+            spark: '3.5'

Review Comment:
   The `verify-on-spark-4.2-binary` matrix entry sets `spark: '3.5'`, which 
makes the build use `-Pspark-3.5` while downloading Spark 4.2.0. This looks 
inconsistent and likely should use the `spark-4.2` profile.



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