You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@spark.apache.org by do...@apache.org on 2026/08/20 20:30:08 UTC

(spark) branch branch-4.3 updated: [SPARK-58915][CORE][K8S][YARN] Add `supportsExecutorHold` to the scheduler backends

This is an automated email from the ASF dual-hosted git repository.

dongjoon-hyun pushed a commit to branch branch-4.3
in repository https://gitbox.apache.org/repos/asf/spark.git


The following commit(s) were added to refs/heads/branch-4.3 by this push:
     new 5ddd052fda7b [SPARK-58915][CORE][K8S][YARN] Add `supportsExecutorHold` to the scheduler backends
5ddd052fda7b is described below

commit 5ddd052fda7bd55666076563c3d707286925e476
Author: Dongjoon Hyun <do...@apache.org>
AuthorDate: Thu Aug 20 13:29:42 2026 -0700

    [SPARK-58915][CORE][K8S][YARN] Add `supportsExecutorHold` to the scheduler backends
    
    ### What changes were proposed in this pull request?
    
    This PR adds `private[spark] CoarseGrainedSchedulerBackend.supportsExecutorHold`, which tells
    whether a backend can hold its executors gracefully: publish an all-zero executor requirement to
    the cluster manager, and let the executors that are already running finish their tasks and exit on
    their own instead of being terminated.
    
    It is `false` by default, so a backend has to opt in:
    
    | Backend | Value | Reason |
    |---|---|---|
    | `CoarseGrainedSchedulerBackend` | `false` | `doRequestTotalExecutors` does not acknowledge the published requirement |
    | `StandaloneSchedulerBackend` | `true` | The Master honors a zero executor limit without killing the running executors |
    | `YarnSchedulerBackend` | `true` | The AM honors a zero executor target without killing the running executors |
    | `KubernetesClusterSchedulerBackend` | `true` (with the default `direct` allocator only). | The other allocators depends on K8s management. |
    
    ### Why are the changes needed?
    
    This is split out of https://github.com/apache/spark/pull/58054, which holds and resumes an
    application through graceful executor decommission. Whether a cluster manager keeps its running
    executors alive on a zero requirement is a per-resource-manager judgement, orthogonal to the rest
    of that PR and spanning Standalone, YARN, and Kubernetes, so it is reviewed on its own.
    
    ### Does this PR introduce _any_ user-facing change?
    
    No because this is a new `private[spark]` method.
    
    ### How was this patch tested?
    
    Pass the CIs.
    
    ### Was this patch authored or co-authored using generative AI tooling?
    
    Generated-by: Claude Opus 5
    
    Closes #58174 from dongjoon-hyun/SPARK-58915.
    
    Authored-by: Dongjoon Hyun <do...@apache.org>
    Signed-off-by: Dongjoon Hyun <do...@apache.org>
    (cherry picked from commit 18c5d1e9dc2a688862d36c8f3b8d5ca815dc4800)
    Signed-off-by: Dongjoon Hyun <do...@apache.org>
---
 .../scheduler/cluster/CoarseGrainedSchedulerBackend.scala  | 11 +++++++++++
 .../scheduler/cluster/StandaloneSchedulerBackend.scala     |  4 ++++
 .../cluster/k8s/KubernetesClusterSchedulerBackend.scala    |  8 ++++++++
 .../k8s/KubernetesClusterSchedulerBackendSuite.scala       | 14 ++++++++++++++
 .../spark/scheduler/cluster/YarnSchedulerBackend.scala     |  4 ++++
 5 files changed, 41 insertions(+)

diff --git a/core/src/main/scala/org/apache/spark/scheduler/cluster/CoarseGrainedSchedulerBackend.scala b/core/src/main/scala/org/apache/spark/scheduler/cluster/CoarseGrainedSchedulerBackend.scala
index 5a9567b3233b..5e48219d2af0 100644
--- a/core/src/main/scala/org/apache/spark/scheduler/cluster/CoarseGrainedSchedulerBackend.scala
+++ b/core/src/main/scala/org/apache/spark/scheduler/cluster/CoarseGrainedSchedulerBackend.scala
@@ -729,6 +729,17 @@ class CoarseGrainedSchedulerBackend(scheduler: TaskSchedulerImpl, val rpcEnv: Rp
     executorDataMap.keySet.toSeq
   }
 
+  /**
+   * Whether this backend can hold its executors gracefully: publish an all-zero executor
+   * requirement to the cluster manager, and let the executors that are already running finish
+   * their tasks and exit on their own instead of being terminated.
+   *
+   * False by default so that a backend has to opt in: the base `doRequestTotalExecutors` does
+   * not acknowledge the published requirement at all, and a cluster manager that terminates
+   * running executors once the requirement drops to zero cannot drain them gracefully.
+   */
+  private[spark] def supportsExecutorHold: Boolean = false
+
   def getExecutorsWithRegistrationTs(): Map[String, Long] = synchronized {
     executorDataMap.toMap.transform((_, v) => v.registrationTs)
   }
diff --git a/core/src/main/scala/org/apache/spark/scheduler/cluster/StandaloneSchedulerBackend.scala b/core/src/main/scala/org/apache/spark/scheduler/cluster/StandaloneSchedulerBackend.scala
index 061b54914c83..751c4def7c92 100644
--- a/core/src/main/scala/org/apache/spark/scheduler/cluster/StandaloneSchedulerBackend.scala
+++ b/core/src/main/scala/org/apache/spark/scheduler/cluster/StandaloneSchedulerBackend.scala
@@ -248,6 +248,10 @@ private[spark] class StandaloneSchedulerBackend(
     }
   }
 
+  // The Master honors a zero executor limit without killing the running executors, so the
+  // executors can be held gracefully.
+  private[spark] override def supportsExecutorHold: Boolean = true
+
   /**
    * Kill the given list of executors through the Master.
    * @return whether the kill request is acknowledged.
diff --git a/resource-managers/kubernetes/core/src/main/scala/org/apache/spark/scheduler/cluster/k8s/KubernetesClusterSchedulerBackend.scala b/resource-managers/kubernetes/core/src/main/scala/org/apache/spark/scheduler/cluster/k8s/KubernetesClusterSchedulerBackend.scala
index 255d76fece56..27a0b320cbf9 100644
--- a/resource-managers/kubernetes/core/src/main/scala/org/apache/spark/scheduler/cluster/k8s/KubernetesClusterSchedulerBackend.scala
+++ b/resource-managers/kubernetes/core/src/main/scala/org/apache/spark/scheduler/cluster/k8s/KubernetesClusterSchedulerBackend.scala
@@ -195,6 +195,14 @@ private[spark] class KubernetesClusterSchedulerBackend(
     Future.successful(true)
   }
 
+  // The Deployment/StatefulSet allocators (and unknown custom ones) scale their controller
+  // down on a zero requirement, which deletes running executor pods after the termination
+  // grace period instead of letting them finish their tasks, so holding is supported only
+  // with the direct allocator.
+  private[spark] override def supportsExecutorHold: Boolean = {
+    conf.get(KUBERNETES_ALLOCATION_PODS_ALLOCATOR) == "direct"
+  }
+
   override def sufficientResourcesRegistered(): Boolean = {
     totalRegisteredExecutors.get() >= minRegisteredExecutors
   }
diff --git a/resource-managers/kubernetes/core/src/test/scala/org/apache/spark/scheduler/cluster/k8s/KubernetesClusterSchedulerBackendSuite.scala b/resource-managers/kubernetes/core/src/test/scala/org/apache/spark/scheduler/cluster/k8s/KubernetesClusterSchedulerBackendSuite.scala
index 5e4ec0ca6115..b330e765e47d 100644
--- a/resource-managers/kubernetes/core/src/test/scala/org/apache/spark/scheduler/cluster/k8s/KubernetesClusterSchedulerBackendSuite.scala
+++ b/resource-managers/kubernetes/core/src/test/scala/org/apache/spark/scheduler/cluster/k8s/KubernetesClusterSchedulerBackendSuite.scala
@@ -357,4 +357,18 @@ class KubernetesClusterSchedulerBackendSuite extends SparkFunSuite with BeforeAn
     assert(id1 === id2, "applicationId() must return the same value on repeated calls")
     assert(id1.startsWith("spark-"), "generated app ID should have the spark- prefix")
   }
+
+  test("SPARK-58915: the executors can be held only with the direct pods allocator") {
+    assert(schedulerBackendUnderTest.supportsExecutorHold)
+    Seq("statefulset", "deployment", "com.example.CustomAllocator").foreach { allocator =>
+      sparkConf.set(KUBERNETES_ALLOCATION_PODS_ALLOCATOR, allocator)
+      try {
+        assert(!schedulerBackendUnderTest.supportsExecutorHold,
+          s"holding must not be supported with the $allocator allocator")
+      } finally {
+        sparkConf.remove(KUBERNETES_ALLOCATION_PODS_ALLOCATOR.key)
+      }
+    }
+    assert(schedulerBackendUnderTest.supportsExecutorHold)
+  }
 }
diff --git a/resource-managers/yarn/src/main/scala/org/apache/spark/scheduler/cluster/YarnSchedulerBackend.scala b/resource-managers/yarn/src/main/scala/org/apache/spark/scheduler/cluster/YarnSchedulerBackend.scala
index b51c52e0e169..efe63895efeb 100644
--- a/resource-managers/yarn/src/main/scala/org/apache/spark/scheduler/cluster/YarnSchedulerBackend.scala
+++ b/resource-managers/yarn/src/main/scala/org/apache/spark/scheduler/cluster/YarnSchedulerBackend.scala
@@ -163,6 +163,10 @@ private[spark] abstract class YarnSchedulerBackend(
     yarnSchedulerEndpointRef.ask[Boolean](prepareRequestExecutors(resourceProfileToTotalExecs))
   }
 
+  // The AM honors a zero executor target without killing the running executors, so the
+  // executors can be held gracefully.
+  private[spark] override def supportsExecutorHold: Boolean = true
+
   /**
    * Request that the ApplicationMaster kill the specified executors.
    */


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@spark.apache.org
For additional commands, e-mail: commits-help@spark.apache.org