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


##########
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:
   Thank you, @MaxGekk. You're right. Addressed in ea8b9997 by narrowing the 
catch to the spec build of the API lookup in `isDriverRequested`, which is 
reached only after the cache check came back empty, and only for a suspended 
application. A spec failure for a cached driver propagates again like before 
this PR and `ClusterInitStep`, which the new 
`suspendedAppWithCachedDriverAndUnbuildableSpecKeepsKueueWorkload` verifies. A 
non-suspended application still gets `SchedulingFailure` for it, see 
`driverMissingFromCacheWithUnbuildableSpecFailsScheduling`.
   



##########
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:
   Yes, the cached `Workload` can lag behind the live read of 
`requestAdmission` too, so this PR narrows that pre-existing window rather than 
closing it. Looking the driver up whenever a `Workload` may exist is affordable 
only if the lookup does not build the driver spec (see your comment on the spec 
build), so I'll handle both in a follow-up JIRA: look up the driver by a name 
derived without the spec, and then do it for every suspended application like 
`ClusterInitStep.isMasterRequested`.
   



##########
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:
   Addressed in ea8b9997. `driverRequested` is computed once in the suspend 
branch and passed to `holdForKueueAdmission`, so a driver found there is not 
looked up again. `verify(mockContext).getCurrentAttemptDriverPod()` in 
`suspendedAppWithDriverMissingFromCacheKeepsItsKueueWorkload` fails without it. 
Note that a pod deleted between the two lookups would still be created again by 
`getOrCreateSecondaryResource` in the same reconcile, so the single evaluation 
saves the call rather than closing that window.
   



##########
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:
   Thank you for the analysis. Right, the spec build uploads the dependencies 
and runs again after `setKueuePodSetFlavors`. The same already happens for the 
lookup of SPARK-59800 and for a cached driver in 
`getCurrentAttemptDriverPod()`, so I'll fix them together in a follow-up JIRA 
by deriving the driver pod name (`spark.kubernetes.driver.pod.name`, or 
`<appId>-driver`) without building the spec. That removes the spec-build 
failure mode behind the catch as well.
   



##########
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:
   Addressed in ea8b9997 with 
`SparkAppContext.isLiveDriverPod(SparkApplication, Pod)`, which the cached 
candidates and the API server pod in `getCurrentAttemptDriverPod()` and the 
lookup in `isDriverRequested` share now. It is static so that the mocked 
contexts in the tests keep the real check. The new 
`podOfAnotherApplicationOnApiServerIsNotCurrentAttemptDriver` covers the API 
server side of `getCurrentAttemptDriverPod()`, which checked only the deletion 
before.
   



##########
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:
   Addressed in ea8b9997. The new 
`suspendedAppWithCachedDriverAndUnbuildableSpecKeepsKueueWorkload` uses the 
real `SparkAppContext` with a cached driver pod whose spec build throws, and 
asserts that the failure propagates and the `Workload` is kept. It fails before 
the change. This test now states that it covers the case without a cached 
driver.
   



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