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

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


The following commit(s) were added to refs/heads/main by this push:
     new 1bd7786  [SPARK-58649] Allow SparkApplication drivers ingress to 
SparkCluster workers
1bd7786 is described below

commit 1bd7786cf6e1408e792c6dd2e4c5e9d1cbeb7102
Author: yannick.goetschel <[email protected]>
AuthorDate: Fri Aug 7 13:43:35 2026 -0700

    [SPARK-58649] Allow SparkApplication drivers ingress to SparkCluster workers
    
    ### What changes were proposed in this pull request?
    
    This PR adds a second ingress peer to the worker `NetworkPolicy` generated 
for every
    `SparkCluster`, so that driver pods in the same namespace are admitted 
alongside the cluster's
    own pods.
    
    ### Why are the changes needed?
    
    We run per-user Spark Connect sessions as SparkApplications attached to a 
shared standalone
    `SparkCluster`. Most jobs are fine, but every so often one dies like this:
    
    ```
    Job aborted due to stage failure: Task 11 in stage 2.0 failed 4 times, most 
recent failure:
    Lost task 11.3 in stage 2.0 (TID 86) (<executor-ip> executor 3):
    TaskResultLost (result lost from block manager)
    ```
    
    What made this confusing is that the tasks themselves succeed. The History 
Server lists them as
    SUCCESS with TaskResultLost sitting in the Errors column, so the compute 
finished and only the
    result transfer failed. It also does not hit every task: in the run above, 
3 of 25 tasks in the
    stage died this way and the other 22 completed normally.
    
    The driver log explains why only some of them:
    
    ```
    java.io.IOException: Connecting to /<executor-ip>:<ephemeral-port> timed 
out (120000 ms)
            at 
org.apache.spark.network.client.TransportClientFactory.createClient(...)
            at 
org.apache.spark.network.netty.NettyBlockTransferService$$anon$2.createAndStart(...)
            at 
org.apache.spark.network.shuffle.RetryingBlockTransferor.transferAllOutstanding(...)
            at 
org.apache.spark.storage.BlockManager.fetchRemoteManagedBuffer(...)
            at org.apache.spark.scheduler.TaskResultGetter$$anon$3.run(...)
    ```
    
    The driver cannot reach the executors' block manager. That only matters 
once a task result goes
    over `spark.task.maxDirectResultSize` (1 MB): below it the result rides 
along inline in the RPC,
    above it the executor parks the result in its block manager and hands the 
driver an
    `IndirectTaskResult` to go and fetch. The tasks that stayed under the limit 
never noticed
    anything. The three that went over timed out, retried three times and gave 
up, and the stage
    aborted on the fourth attempt.
    
    The culprit is the worker NetworkPolicy generated in
    `SparkClusterResourceSpec#buildWorkerNetworkPolicy`:
    
    ```yaml
    podSelector:
      matchLabels: {spark-role: worker, spark.operator/spark-cluster-name: 
<cluster>}
    ingress:
    - from:
      - podSelector:
          matchLabels: {spark.operator/spark-cluster-name: <cluster>}
    policyTypes: [Ingress]
    ```
    
    Only pods carrying the cluster label may talk to the workers. But a 
SparkApplication attached to
    the cluster is a separate resource: its driver is labelled `spark-role: 
driver` and
    `spark.operator/spark-app-name: <app>`, never the cluster label. So the 
driver is locked out of
    its own executors.
    
    None of this is visible from the outside. Worker egress is unrestricted and 
no policy selects
    driver pods, so the executor-to-driver direction works perfectly well — 
executors register,
    tasks get dispatched, tasks run. Only the return path for large results is 
broken, so the
    cluster looks healthy right up until a job happens to cross the 1 MB 
threshold.
    
    There is no way around it from the client side either. Executors read
    `spark.task.maxDirectResultSize` from the driver SparkConf when they start, 
so a Connect client
    cannot raise it partway through a session.
    
    Ports are left unrestricted on the new peer because block manager ports are 
ephemeral, and
    pinning `spark.blockManager.port` is not workable when several executors 
share a worker pod.
    
    ### Does this PR introduce _any_ user-facing change?
    
    Yes. The generated worker `NetworkPolicy` now also admits driver pods in 
the same namespace.
    Since NetworkPolicies are additive this can only widen ingress, so it 
cannot deny traffic that
    was previously allowed. Clusters running on a CNI that does not enforce 
NetworkPolicy are
    unaffected either way.
    
    ### How was this patch tested?
    
    Extended `SparkClusterResourceSpecTest#testWorkerNetworkPolicy` to assert 
both ingress peers.
    
    ```
    ./gradlew :spark-submission-worker:test --tests 
'*SparkClusterResourceSpecTest*'
    ./gradlew :spark-submission-worker:spotlessCheck
    ```
    
    Both pass. We also applied the equivalent policy by hand against operator 
1.0.0, and the job
    that reproduced this now completes.
    
    ### Was this patch authored or co-authored using generative AI tooling?
    
    NO
    
    Closes #787 from YGoetschel/worker-networkpolicy-allow-drivers.
    
    Authored-by: yannick.goetschel <[email protected]>
    Signed-off-by: Dongjoon Hyun <[email protected]>
---
 .../apache/spark/k8s/operator/SparkClusterResourceSpec.java    | 10 ++++++++++
 .../spark/k8s/operator/SparkClusterResourceSpecTest.java       |  6 +++++-
 2 files changed, 15 insertions(+), 1 deletion(-)

diff --git 
a/spark-submission-worker/src/main/java/org/apache/spark/k8s/operator/SparkClusterResourceSpec.java
 
b/spark-submission-worker/src/main/java/org/apache/spark/k8s/operator/SparkClusterResourceSpec.java
index 70eb6c4..e7319b1 100644
--- 
a/spark-submission-worker/src/main/java/org/apache/spark/k8s/operator/SparkClusterResourceSpec.java
+++ 
b/spark-submission-worker/src/main/java/org/apache/spark/k8s/operator/SparkClusterResourceSpec.java
@@ -462,6 +462,11 @@ public class SparkClusterResourceSpec {
   /**
    * Builds the NetworkPolicy for the SparkCluster.
    *
+   * <p>Ingress is allowed from the cluster's own pods and from driver pods in 
the same namespace.
+   * Drivers are admitted because a SparkApplication attaching to this cluster 
is a separate
+   * resource and does not carry the cluster label, yet the driver must reach 
the executors' block
+   * manager to fetch task results larger than {@code 
spark.task.maxDirectResultSize}.
+   *
    * @param clusterName The name of the SparkCluster.
    * @param namespace The namespace of the SparkApplication.
    * @return A NetworkPolicy object.
@@ -484,6 +489,11 @@ public class SparkClusterResourceSpec {
         .addToMatchLabels(LABEL_SPARK_CLUSTER_NAME, clusterName)
         .endPodSelector()
         .endFrom()
+        .addNewFrom()
+        .withNewPodSelector()
+        .addToMatchLabels(LABEL_SPARK_ROLE_NAME, LABEL_SPARK_ROLE_DRIVER_VALUE)
+        .endPodSelector()
+        .endFrom()
         .endIngress()
         .endSpec()
         .build();
diff --git 
a/spark-submission-worker/src/test/java/org/apache/spark/k8s/operator/SparkClusterResourceSpecTest.java
 
b/spark-submission-worker/src/test/java/org/apache/spark/k8s/operator/SparkClusterResourceSpecTest.java
index 63c4171..50a9c55 100644
--- 
a/spark-submission-worker/src/test/java/org/apache/spark/k8s/operator/SparkClusterResourceSpecTest.java
+++ 
b/spark-submission-worker/src/test/java/org/apache/spark/k8s/operator/SparkClusterResourceSpecTest.java
@@ -329,8 +329,12 @@ class SparkClusterResourceSpecTest {
     assertEquals(expected, policy.getSpec().getPodSelector().getMatchLabels());
     assertTrue(policy.getSpec().getEgress().isEmpty());
     assertEquals(1, policy.getSpec().getIngress().size());
+    var from = policy.getSpec().getIngress().get(0).getFrom();
+    assertEquals(2, from.size());
     assertEquals(Map.of(LABEL_SPARK_CLUSTER_NAME, "cluster-name"),
-        
policy.getSpec().getIngress().get(0).getFrom().get(0).getPodSelector().getMatchLabels());
+        from.get(0).getPodSelector().getMatchLabels());
+    assertEquals(Map.of(LABEL_SPARK_ROLE_NAME, LABEL_SPARK_ROLE_DRIVER_VALUE),
+        from.get(1).getPodSelector().getMatchLabels());
   }
 
   @Test


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

Reply via email to