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 54b25b6678e4 [SPARK-60079][K8S][TEST] Add `ExecutorResizePlugin` 
integration test
54b25b6678e4 is described below

commit 54b25b6678e4abe2b74605b2f64bad763777cdb2
Author: Dongjoon Hyun <[email protected]>
AuthorDate: Thu Oct 8 05:02:07 2026 -0700

    [SPARK-60079][K8S][TEST] Add `ExecutorResizePlugin` integration test
    
    ### What changes were proposed in this pull request?
    
    This PR aims to add an integration test for `ExecutorResizePlugin`.
    
    The test runs `JavaSparkSQLCli` with `SELECT reflect('java.lang.Thread', 
'sleep', 600000L)` so that the executor stays alive while the plugin works.
    
    The plugin uses the following configurations:
    - `spark.kubernetes.executor.resizeInterval=5s`
    - `spark.kubernetes.executor.resizeThreshold=0.01`, so that any running 
executor exceeds it.
    - `spark.kubernetes.executor.resizeFactor=0.1`
    - `spark.kubernetes.executor.resizeMaxMemory=1536m`, which is lower than 
the first increased limit (`1408Mi * 1.1`). So, the first resize reaches the 
maximum.
    
    The test verifies the following:
    - The driver log shows that executor 1 is resized to `1536Mi`. A later log 
line shows that the next resize is skipped because the maximum was reached.
    - The executor pod spec has `1536Mi` as both the memory request and the 
memory limit.
    - The container status reports `1536Mi` as the memory limit. This means the 
kubelet applied the resize in place.
    - The container restart count is `0`.
    
    The test is skipped when the `metrics.k8s.io` API is unavailable. It is 
also skipped in the `KubernetesSuite` subclasses like `VolcanoSuite`, because 
the resize does not depend on the scheduler.
    
    ### Why are the changes needed?
    
    `ExecutorResizePlugin` has only unit tests with a mocked 
`KubernetesClient`. This PR adds a real K8s cluster test to cover the following:
    - Reading memory usage through the `metrics.k8s.io` API.
    - Patching the `resize` subresource.
    - The in-place resize by the kubelet.
    
    This will provide the test coverage.
    - https://github.com/apache/spark/pull/54215
    - https://github.com/apache/spark/pull/58582
    - https://github.com/apache/spark/pull/58711
    - https://github.com/apache/spark/pull/59193
    - https://github.com/apache/spark/pull/59281
    
    ### Does this PR introduce _any_ user-facing change?
    
    No. This is a test-only change.
    
    ### How was this patch tested?
    
    Pass the CIs.
    
    ```
    [info] - SPARK-60079: Resize executor memory in place with 
ExecutorResizePlugin (29 seconds, 130 milliseconds)
    ```
    
    ### Was this patch authored or co-authored using generative AI tooling?
    
    Generated-by: Claude Opus 5.5
    
    Closes #59289 from dongjoon-hyun/SPARK-60079.
    
    Authored-by: Dongjoon Hyun <[email protected]>
    Signed-off-by: Dongjoon Hyun <[email protected]>
---
 .../k8s/integrationtest/ExecutorResizeSuite.scala  | 87 ++++++++++++++++++++++
 .../k8s/integrationtest/KubernetesSuite.scala      |  3 +-
 2 files changed, 89 insertions(+), 1 deletion(-)

diff --git 
a/resource-managers/kubernetes/integration-tests/src/test/scala/org/apache/spark/deploy/k8s/integrationtest/ExecutorResizeSuite.scala
 
b/resource-managers/kubernetes/integration-tests/src/test/scala/org/apache/spark/deploy/k8s/integrationtest/ExecutorResizeSuite.scala
new file mode 100644
index 000000000000..326e7418b76d
--- /dev/null
+++ 
b/resource-managers/kubernetes/integration-tests/src/test/scala/org/apache/spark/deploy/k8s/integrationtest/ExecutorResizeSuite.scala
@@ -0,0 +1,87 @@
+/*
+ * 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.deploy.k8s.integrationtest
+
+import scala.jdk.CollectionConverters._
+
+import io.fabric8.kubernetes.api.model.Quantity
+import org.scalatest.concurrent.Eventually
+
+import org.apache.spark.internal.config.PLUGINS
+
+private[spark] trait ExecutorResizeSuite { k8sSuite: KubernetesSuite =>
+
+  import ExecutorResizeSuite._
+  import KubernetesSuite.{k8sTestTag, INTERVAL, SPARK_SQL_CLI_MAIN_CLASS, 
TIMEOUT}
+
+  test("SPARK-60079: Resize executor memory in place with 
ExecutorResizePlugin", k8sTestTag) {
+    // The resize does not depend on the scheduler, so skip the reruns in 
subclasses.
+    assume(this.getClass.getSimpleName == "KubernetesSuite")
+    
assume(kubernetesTestComponents.kubernetesClient.hasApiGroup("metrics.k8s.io", 
true),
+      "ExecutorResizePlugin requires the metrics.k8s.io API")
+    sparkAppConf
+      .set(PLUGINS.key, "ExecutorResizePlugin")
+      .set("spark.kubernetes.executor.resizeInterval", "5s")
+      // Even an idle executor exceeds this threshold.
+      .set("spark.kubernetes.executor.resizeThreshold", "0.01")
+      .set("spark.kubernetes.executor.resizeFactor", "0.1")
+      // Lower than the first increased limit, so the first resize reaches the 
maximum.
+      .set("spark.kubernetes.executor.resizeMaxMemory", "1536m")
+
+    runSparkApplicationAndVerifyCompletion(
+      appResource = containerLocalSparkDistroExamplesJar,
+      mainClass = SPARK_SQL_CLI_MAIN_CLASS,
+      expectedDriverLogOnCompletion = Seq(
+        "Initialized driver component for plugin " +
+          "org.apache.spark.scheduler.cluster.k8s.ExecutorResizePlugin",
+        "Increase executor 1 container memory from",
+        s"to $MAX_MEMORY as usage",
+        s"Skip resizing executor 1 as container memory limit $MAX_MEMORY 
already reached " +
+          s"the maximum $MAX_MEMORY"),
+      // Keep the application running until the plugin resizes the executor.
+      appArgs = Array("SELECT reflect('java.lang.Thread', 'sleep', 600000L)"),
+      driverPodChecker = doBasicDriverPodCheck,
+      executorPodChecker = doBasicExecutorPodCheck,
+      isJVM = true)
+
+    // The application is still running, so the executor pod is alive.
+    val pods = kubernetesTestComponents.kubernetesClient
+      .pods()
+      .inNamespace(kubernetesTestComponents.namespace)
+      .withLabel("spark-app-locator", appLocator)
+      .withLabel("spark-role", "executor")
+      .withLabel("spark-exec-id", "1")
+    Eventually.eventually(TIMEOUT, INTERVAL) {
+      val pod = pods.list().getItems.get(0)
+      val container = pod.getSpec.getContainers.asScala
+        .find(_.getName == "spark-kubernetes-executor").get
+      assert(toBytes(container.getResources.getLimits.get("memory")) === 
MAX_MEMORY)
+      assert(toBytes(container.getResources.getRequests.get("memory")) === 
MAX_MEMORY)
+      // The kubelet applies the new limit without restarting the container.
+      val status = pod.getStatus.getContainerStatuses.asScala
+        .find(_.getName == "spark-kubernetes-executor").get
+      assert(toBytes(status.getResources.getLimits.get("memory")) === 
MAX_MEMORY)
+      assert(status.getRestartCount === 0)
+    }
+  }
+
+  private def toBytes(quantity: Quantity): Long = 
Quantity.getAmountInBytes(quantity).longValue()
+}
+
+private[spark] object ExecutorResizeSuite {
+  val MAX_MEMORY: Long = 1536L * 1024 * 1024
+}
diff --git 
a/resource-managers/kubernetes/integration-tests/src/test/scala/org/apache/spark/deploy/k8s/integrationtest/KubernetesSuite.scala
 
b/resource-managers/kubernetes/integration-tests/src/test/scala/org/apache/spark/deploy/k8s/integrationtest/KubernetesSuite.scala
index 624d5b2dae2c..afc769931f09 100644
--- 
a/resource-managers/kubernetes/integration-tests/src/test/scala/org/apache/spark/deploy/k8s/integrationtest/KubernetesSuite.scala
+++ 
b/resource-managers/kubernetes/integration-tests/src/test/scala/org/apache/spark/deploy/k8s/integrationtest/KubernetesSuite.scala
@@ -44,7 +44,7 @@ class KubernetesSuite extends SparkFunSuite
   with BeforeAndAfterAll with BeforeAndAfter with BasicTestsSuite with 
SparkConfPropagateSuite
   with SecretsTestsSuite with PythonTestsSuite with ClientModeTestsSuite with 
PodTemplateSuite
   with VolumeSuite with PVTestsSuite with DepsTestsSuite with 
DecommissionSuite with RTestsSuite
-  with Logging with Eventually with Matchers {
+  with ExecutorResizeSuite with Logging with Eventually with Matchers {
 
 
   import KubernetesSuite._
@@ -658,6 +658,7 @@ private[spark] object KubernetesSuite {
   val SPARK_MINI_READ_WRITE_TEST = 
"org.apache.spark.examples.MiniReadWriteTest"
   val SPARK_REMOTE_MAIN_CLASS: String = 
"org.apache.spark.examples.SparkRemoteFileTest"
   val SPARK_DRIVER_MAIN_CLASS: String = 
"org.apache.spark.examples.DriverSubmissionTest"
+  val SPARK_SQL_CLI_MAIN_CLASS: String = 
"org.apache.spark.examples.sql.JavaSparkSQLCli"
   val TIMEOUT = PatienceConfiguration.Timeout(Span(3, Minutes))
   val INTERVAL = PatienceConfiguration.Interval(Span(1, Seconds))
 }


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

Reply via email to