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

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


The following commit(s) were added to refs/heads/master by this push:
     new 0ccd72732e2d [SPARK-59839][K8S] Prune PVC usage reports of removed and 
inactive executors in `ExecutorPVCResizePlugin`
0ccd72732e2d is described below

commit 0ccd72732e2d2c98b52a24c7503a97274e35bcba
Author: Dongjoon Hyun <[email protected]>
AuthorDate: Tue Sep 29 05:53:05 2026 -0700

    [SPARK-59839][K8S] Prune PVC usage reports of removed and inactive 
executors in `ExecutorPVCResizePlugin`
    
    ### What changes were proposed in this pull request?
    
    This PR aims to prune the PVC usage reports of removed and inactive 
executors in `ExecutorPVCResizePlugin`.
    - Exclude inactive executor pods (`spark-exec-inactive=true`) from the pod 
listing, like `ExecutorPodsPollingSnapshotSource` and 
`ExecutorPodsWatchSnapshotSource`.
    - Drop the reports of executors without a listed pod, like 
`ExecutorResizePlugin` does for `cappedExecutors`.
    
    Note that only `latestReports` grows per executor. The PVC-name-keyed maps 
(`failedPvcs`, `cappedPvcs`, and `requestedSizes`) are intentionally out of 
scope because PVCs can outlive executors via PVC reuse, and they are neither 
iterated nor logged.
    
    ### Why are the changes needed?
    
    `latestReports` is never pruned. So, in long-running applications with 
dynamic allocation, the reports of removed executors accumulate forever and the 
whole map is logged at INFO level every interval.
    
    In addition, with `spark.kubernetes.executor.deleteOnTermination=false`, 
the stale reports of terminated executor pods keep being applied to their PVCs, 
which can be reused by new executors.
    
    ### Does this PR introduce _any_ user-facing change?
    
    Yes, previously it was never pruned. This is a bug fix for 
`ExecutorPVCResizePlugin`, which was released in 
[v4.2.0](https://github.com/apache/spark/releases/tag/v4.2.0) (2026-07-11).
    
    ### How was this patch tested?
    
    Pass the CIs with the newly added test cases.
    
    ### Was this patch authored or co-authored using generative AI tooling?
    
    Generated-by: Claude Opus 5.5
    
    Closes #59112 from dongjoon-hyun/SPARK-59839.
    
    Authored-by: Dongjoon Hyun <[email protected]>
    Signed-off-by: Dongjoon Hyun <[email protected]>
---
 .../cluster/k8s/ExecutorPVCResizePlugin.scala      |  9 +++++++--
 .../cluster/k8s/ExecutorPVCResizePluginSuite.scala | 23 ++++++++++++++++++++++
 2 files changed, 30 insertions(+), 2 deletions(-)

diff --git 
a/resource-managers/kubernetes/core/src/main/scala/org/apache/spark/scheduler/cluster/k8s/ExecutorPVCResizePlugin.scala
 
b/resource-managers/kubernetes/core/src/main/scala/org/apache/spark/scheduler/cluster/k8s/ExecutorPVCResizePlugin.scala
index 2b61de9e645b..43922e1c0dfe 100644
--- 
a/resource-managers/kubernetes/core/src/main/scala/org/apache/spark/scheduler/cluster/k8s/ExecutorPVCResizePlugin.scala
+++ 
b/resource-managers/kubernetes/core/src/main/scala/org/apache/spark/scheduler/cluster/k8s/ExecutorPVCResizePlugin.scala
@@ -67,7 +67,7 @@ class ExecutorPVCResizeDriverPlugin extends DriverPlugin with 
Logging {
   private var factor: Double = _
   private var maxStorage: Long = Long.MaxValue
 
-  private val latestReports = new ConcurrentHashMap[String, 
PVCDiskUsageReport]()
+  private[k8s] val latestReports = new ConcurrentHashMap[String, 
PVCDiskUsageReport]()
   private val failedPvcs = ConcurrentHashMap.newKeySet[String]()
   // PVCs whose storage request already reached maxStorage, to log the skip 
only once.
   private val cappedPvcs = ConcurrentHashMap.newKeySet[String]()
@@ -121,16 +121,17 @@ class ExecutorPVCResizeDriverPlugin extends DriverPlugin 
with Logging {
   }
 
   private[k8s] def checkAndResizePVCs(): Unit = {
-    logInfo(s"Latest PVC usage reports: $latestReports")
     val appId = sparkContext.applicationId
 
     sparkContext.schedulerBackend match {
       case b: KubernetesClusterSchedulerBackend =>
         val client = b.kubernetesClient
+        // Skip terminated pods kept by deleteOnTermination=false since their 
reports are stale.
         val pods = client.pods()
           .inNamespace(namespace)
           .withLabel(SPARK_APP_ID_LABEL, appId)
           .withLabel(SPARK_ROLE_LABEL, SPARK_POD_EXECUTOR_ROLE)
+          .withoutLabel(SPARK_EXECUTOR_INACTIVE_LABEL, "true")
           .list()
           .getItems.asScala
 
@@ -138,6 +139,10 @@ class ExecutorPVCResizeDriverPlugin extends DriverPlugin 
with Logging {
           Option(p.getMetadata.getLabels.get(SPARK_EXECUTOR_ID_LABEL)).map(_ 
-> p)
         }.toMap
 
+        // Drop reports of executors without a pod so that latestReports does 
not grow unbounded.
+        latestReports.keySet().retainAll(podByExecId.keySet.asJava)
+        logInfo(s"Latest PVC usage reports: $latestReports")
+
         latestReports.values().asScala.foreach { report =>
           podByExecId.get(report.executorId).foreach { pod =>
             pvcsOf(pod).foreach { pvcName =>
diff --git 
a/resource-managers/kubernetes/core/src/test/scala/org/apache/spark/scheduler/cluster/k8s/ExecutorPVCResizePluginSuite.scala
 
b/resource-managers/kubernetes/core/src/test/scala/org/apache/spark/scheduler/cluster/k8s/ExecutorPVCResizePluginSuite.scala
index 55fb843016ba..f446d24c3d92 100644
--- 
a/resource-managers/kubernetes/core/src/test/scala/org/apache/spark/scheduler/cluster/k8s/ExecutorPVCResizePluginSuite.scala
+++ 
b/resource-managers/kubernetes/core/src/test/scala/org/apache/spark/scheduler/cluster/k8s/ExecutorPVCResizePluginSuite.scala
@@ -66,6 +66,7 @@ class ExecutorPVCResizePluginSuite
     when(podOperations.inNamespace(namespace)).thenReturn(podsWithNamespace)
     when(podsWithNamespace.withLabel(SPARK_APP_ID_LABEL, 
appId)).thenReturn(labeledPods)
     when(labeledPods.withLabel(SPARK_ROLE_LABEL, 
SPARK_POD_EXECUTOR_ROLE)).thenReturn(labeledPods)
+    when(labeledPods.withoutLabel(SPARK_EXECUTOR_INACTIVE_LABEL, 
"true")).thenReturn(labeledPods)
     when(labeledPods.list()).thenReturn(podList)
     when(kubernetesClient.persistentVolumeClaims()).thenReturn(pvcOperations)
     when(pvcOperations.inNamespace(namespace)).thenReturn(pvcsWithNamespace)
@@ -308,6 +309,28 @@ class ExecutorPVCResizePluginSuite
     verify(pvcsWithNamespace, 
never()).withName(org.mockito.ArgumentMatchers.anyString())
   }
 
+  test("SPARK-59839: Reports of executors without a pod are removed") {
+    val plugin = createPlugin()
+    val pod = createPodWithPVC(1, "pvc-1", "/data")
+    when(podList.getItems).thenReturn(Collections.singletonList(pod))
+    plugin.receive(PVCDiskUsageReport("1", 0.5))
+    plugin.receive(PVCDiskUsageReport("2", 0.5)) // No pod for executor 2
+    assert(plugin.latestReports.keySet() === java.util.Set.of("1", "2"))
+
+    plugin.checkAndResizePVCs()
+
+    assert(plugin.latestReports.keySet() === Collections.singleton("1"))
+  }
+
+  test("SPARK-59839: Inactive executor pods are excluded from the listing") {
+    val plugin = createPlugin()
+    when(podList.getItems).thenReturn(Collections.emptyList())
+
+    plugin.checkAndResizePVCs()
+
+    verify(labeledPods).withoutLabel(SPARK_EXECUTOR_INACTIVE_LABEL, "true")
+  }
+
   test("pvcsOf returns claim names mounted by the executor container") {
     val plugin = createPlugin()
     val pod = createPodWithPVC(7, "pvc-7", "/spark-local")


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to