MaxGekk commented on code in PR #895:
URL: 
https://github.com/apache/spark-kubernetes-operator/pull/895#discussion_r4118601506


##########
spark-operator/src/main/java/org/apache/spark/k8s/operator/reconciler/reconcilesteps/AppInitStep.java:
##########
@@ -73,17 +74,24 @@ public ReconcileProgress reconcile(
     }
     SparkApplication app = context.getResource();
     if (app.getSpec().isSuspend()) {
-      Optional<Pod> currentAttemptDriverPod;
+      boolean driverRequested = false;
       try {
-        currentAttemptDriverPod = context.getCurrentAttemptDriverPod();
+        driverRequested = isDriverRequested(context);
       } catch (KubernetesClientException e) {
         // Whether a driver of this attempt is live is unknown, not answered. 
Holding would claim
         // in an event that none was requested, and would do so for the whole 
suspend hold
-        // interval, so look again with the steady-state interval instead.
+        // interval. It would also release the Kueue quota of a running 
driver, so look again with
+        // the steady-state interval instead.
         log.warn("Failed to verify the driver pod of a suspended application, 
will retry.", e);
         return completeAndDefaultRequeue();
+      } catch (Exception e) {

Review Comment:
   This catch also covers the case the comment says can't happen: 
`getCurrentAttemptDriverPod()` builds the driver spec exactly when the informer 
cache holds a live driver-labelled pod (it needs the spec for the pod name). If 
the build fails there - the user changed `sparkConf` in the same edit that set 
`suspend: true`, a transient `IOException` from the upload filesystem, an NPE - 
`driverRequested` stays false and `holdForSuspend` releases the admitted 
Workload while the cached driver and its executors keep running outside the 
quota. Before this change the exception propagated and the reconcile was 
retried, and `ClusterInitStep.isMasterRequested` still lets spec-build failures 
propagate.
   
   Could the catch be narrowed to the API-lookup branch of `isDriverRequested`, 
after the cache check has come back empty?



##########
spark-operator/src/main/java/org/apache/spark/k8s/operator/reconciler/reconcilesteps/AppInitStep.java:
##########
@@ -73,17 +74,24 @@ public ReconcileProgress reconcile(
     }
     SparkApplication app = context.getResource();
     if (app.getSpec().isSuspend()) {
-      Optional<Pod> currentAttemptDriverPod;
+      boolean driverRequested = false;
       try {
-        currentAttemptDriverPod = context.getCurrentAttemptDriverPod();
+        driverRequested = isDriverRequested(context);

Review Comment:
   For a suspended app whose driver is found here, the lookup runs a second 
time in the same reconcile, in `holdForKueueAdmission` (line 208, or through 
`handleDequeuedWorkload`). If the second answer differs - the pod was deleted 
in between, or the cache and API answers change - it returns false, and the 
code falls through to `holdForAdmission`, which with an admitted Workload 
returns empty, so the driver is created for an application with 
`spec.suspend=true`. In the normal case the second GET is just a wasted call. 
Could `driverRequested` be computed once and passed down?



##########
spark-operator/src/main/java/org/apache/spark/k8s/operator/reconciler/reconcilesteps/AppInitStep.java:
##########
@@ -239,12 +250,19 @@ private boolean isDriverRequested(SparkAppContext 
context) {
     Optional<Workload> workload = context.getCachedKueueWorkload();
     if (workload.isEmpty()
         || !KueueWorkloadUtils.isAdmitted(workload.get())
-        || (KueueWorkloadUtils.findEviction(workload.get()).isEmpty()
+        || (!context.getResource().getSpec().isSuspend()
+            && KueueWorkloadUtils.findEviction(workload.get()).isEmpty()
             && !KueueWorkloadUtils.isDeactivated(workload.get()))) {
       return false;
     }
     Pod driverPod = 
context.getClient().resource(context.getDriverPodSpec()).get();
-    return driverPod != null && driverPod.getMetadata().getDeletionTimestamp() 
== null;
+    return driverPod != null

Review Comment:
   Nit: "carries the driver labels and is not being deleted" is now written out 
a third time - `SparkAppContext.getDriverPod` and `getCurrentAttemptDriverPod` 
have the same `containsAll(driverLabels(...))` + `deletionTimestamp == null` 
filter. If how a pod is recognised as the current attempt's driver changes 
later (say, an attempt label) and one copy is missed, the cache path and the 
API path disagree about what the driver is, which is the disagreement this PR 
removes. Maybe a shared helper such as 
`SparkAppContext.isCurrentAttemptDriver(Pod)`?



##########
spark-operator/src/main/java/org/apache/spark/k8s/operator/reconciler/reconcilesteps/AppInitStep.java:
##########
@@ -239,12 +250,19 @@ private boolean isDriverRequested(SparkAppContext 
context) {
     Optional<Workload> workload = context.getCachedKueueWorkload();
     if (workload.isEmpty()

Review Comment:
   The API lookup is gated on the *cached* Workload being admitted, but 
admission itself is read from the API server (`requestAdmission` -> 
`getOrCreateSecondaryResource`). So the same cache-lag race this PR fixes for 
pods can come back through the Workload informer: reconcile N reads the 
admitted Workload from the API, creates the driver, and the `DriverRequested` 
status update fails; the user sets `suspend: true`; at N+1 neither the pod 
cache nor the Workload cache has caught up (the Workload may still be the 
pre-admission copy, or not cached at all if it was created in N-1). 
`getCachedKueueWorkload()` is then empty or not admitted, this returns false 
without a lookup, and `releaseWorkload` deletes the admitted Workload by name 
through the API while the driver runs.
   
   For a suspended app, would it be safer to look the driver up whenever a 
Workload may exist (queue label present or Workload cached), as 
`ClusterInitStep.isMasterRequested` does?



##########
spark-operator/src/main/java/org/apache/spark/k8s/operator/reconciler/reconcilesteps/AppInitStep.java:
##########
@@ -239,12 +250,19 @@ private boolean isDriverRequested(SparkAppContext 
context) {
     Optional<Workload> workload = context.getCachedKueueWorkload();
     if (workload.isEmpty()
         || !KueueWorkloadUtils.isAdmitted(workload.get())
-        || (KueueWorkloadUtils.findEviction(workload.get()).isEmpty()
+        || (!context.getResource().getSpec().isSuspend()
+            && KueueWorkloadUtils.findEviction(workload.get()).isEmpty()
             && !KueueWorkloadUtils.isDeactivated(workload.get()))) {
       return false;
     }
     Pod driverPod = 
context.getClient().resource(context.getDriverPodSpec()).get();

Review Comment:
   Building the full driver spec here has side effects for a suspended app: the 
submission worker uploads dependencies to `spark.kubernetes.file.upload.path` 
and cleans up temp resources, only to decide whether to hold it. And when a 
driver is found, `applyAdmittedFlavors` -> `setKueuePodSetFlavors` resets the 
memoized spec, so it's built (and uploaded) again in the same reconcile. The 
lookup only needs the driver pod's name, which is deterministic 
(`SparkAppSubmissionWorker.generateSparkAppId`); a label-selected GET like 
`SparkAppContext.getDriverPodFromApi()`, or the name derived without building 
the whole spec, would avoid both the side effects and the spec-build failure 
mode behind the catch-all at line 87.



##########
spark-operator/src/test/java/org/apache/spark/k8s/operator/reconciler/reconcilesteps/AppInitStepTest.java:
##########
@@ -1428,6 +1431,173 @@ void neverAdmittedKueueWorkloadDoesNotLookUpTheDriver() 
{
     verify(recorder, never()).persistStatus(any(), any());
   }
 
+  @ParameterizedTest
+  @ValueSource(booleans = {false, true})
+  void suspendedAppWithDriverMissingFromCacheKeepsItsKueueWorkload(boolean 
queueLabelRemoved) {
+    // The driver was created, but the status update to DriverRequested did 
not land, and the app
+    // is suspended before the informer has seen the pod. The suspend hold 
would release the
+    // Workload under the driver, even if the queue label was removed 
meanwhile.
+    AppInitStep appInitStep = new AppInitStep();
+    SparkAppContext mockContext = mock(SparkAppContext.class);
+    SparkAppStatusRecorder recorder = mock(SparkAppStatusRecorder.class);
+    SparkApplication application = new SparkApplication();
+    application.setMetadata(kueueApplicationMetadata);
+    
kubernetesClient.resource(KueueWorkloadFactory.buildWorkload(application)).create();
+    admitWorkload();
+    createDriverPod(application);
+    application.getSpec().setSuspend(true);
+    if (queueLabelRemoved) {
+      application.getMetadata().setLabels(Map.of());
+    }
+    when(mockContext.getResource()).thenReturn(application);
+    when(mockContext.getClient()).thenReturn(kubernetesClient);
+    when(mockContext.getEventRecorder()).thenReturn(eventRecorder);
+    
when(mockContext.getCachedKueueWorkload()).thenReturn(Optional.of(getWorkload()));
+    
when(mockContext.getCurrentAttemptDriverPod()).thenReturn(Optional.empty());
+    when(mockContext.getDriverPreResourcesSpec()).thenReturn(List.of());
+    when(mockContext.getDriverPodSpec()).thenReturn(driverPodSpec);
+    when(mockContext.getDriverResourcesSpec()).thenReturn(List.of());
+    when(recorder.persistStatus(any(), any()))
+        .thenAnswer(
+            invocation -> {
+              application.setStatus(invocation.getArgument(1));
+              return true;
+            });
+
+    ReconcileProgress progress = appInitStep.reconcile(mockContext, recorder);
+
+    // The live driver is found, so the initialization completes and the 
Workload which holds its
+    // quota is not released
+    Assertions.assertEquals(ReconcileProgress.completeAndDefaultRequeue(), 
progress);
+    Assertions.assertNotNull(getWorkload());
+    Assertions.assertEquals(
+        ApplicationStateSummary.DriverRequested,
+        application.getStatus().getCurrentState().getCurrentStateSummary());
+    // Like a driver found in the cache, it gets the flavors of the admitted 
Workload again
+    verify(mockContext).setKueuePodSetFlavors(any());
+  }
+
+  @Test
+  void suspendedAppWithUnbuildableDriverSpecReleasesKueueWorkload() {

Review Comment:
   This test stubs `getCurrentAttemptDriverPod()` to return empty, so the real 
path where a driver-labelled pod is cached and `getDriverPodSpec()` throws 
inside `getCurrentAttemptDriverPod()` isn't exercised (see the comment at 
`AppInitStep.java:87`). The `assertNull(getWorkload())` also makes "release on 
spec failure" the intended behaviour for every path. Could there be a test with 
a cached driver pod whose spec build throws, asserting the Workload is kept?



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