peter-toth commented on code in PR #857:
URL: 
https://github.com/apache/spark-kubernetes-operator/pull/857#discussion_r4061719940


##########
spark-operator/src/main/java/org/apache/spark/k8s/operator/reconciler/reconcilesteps/AppInitStep.java:
##########
@@ -194,14 +222,16 @@ private Optional<ReconcileProgress> holdForKueueAdmission(
    * Checks whether the driver pod of the current attempt has already been 
requested. This covers
    * the case where the driver was created but the status update to 
DriverRequested failed, so that
    * a suspended application still completes its initialization instead of 
being held with a live
-   * driver. See {@link SparkAppContext#getCurrentAttemptDriverPod()} for how 
a pod left from a
-   * previous attempt is told apart.
+   * driver. See {@link SparkAppContext#getCurrentAttemptDriverPodStrictly()} 
for how a pod left
+   * from a previous attempt is told apart.
    *
    * @param context The SparkAppContext for the application.
    * @return True if the driver pod of the current attempt exists, false 
otherwise.
+   * @throws KubernetesClientException if the lookup fails, so that a running 
driver is not
+   *     mistaken for one that was never requested.
    */
   private boolean isDriverRequested(SparkAppContext context) {
-    return context.getCurrentAttemptDriverPod().isPresent();
+    return context.getCurrentAttemptDriverPodStrictly().isPresent();

Review Comment:
   **Finding 5.** Nothing pins the throw this line depends on. I replaced the 
body of `getCurrentAttemptDriverPodStrictly` with a version that catches 
`KubernetesClientException` and returns `Optional.empty()` — the lenient 
behaviour this PR exists to get off the app path — and the entire 
`:spark-operator` suite passed.
   
   Why the five new tests do not cover it: they all stub a 
`mock(SparkAppContext.class)`, so 
`when(mockContext.getCurrentAttemptDriverPodStrictly()).thenThrow(...)` tests 
`AppInitStep`'s reaction to a throwing context, never that the real method 
throws. And every one of the seven `SparkAppContextTest` cases calls the 
lenient `getCurrentAttemptDriverPod()`, including 
`apiErrorDuringVerificationIsTreatedAsAbsent`, which asserts the *opposite* 
contract. So the strict variant has no caller in any test.
   
   The cluster side does not have this hole: `isMasterRequested` is real code 
in `ClusterInitStep`, and `suspendedClusterWithUnverifiableMasterIsNotHeld` 
makes the client itself throw, so the read and the decision are covered 
together.
   
   This is new rather than something I missed last round — `e90b0c6` had 
`SparkAppContextTest.apiErrorDuringVerificationIsPropagated` doing exactly 
this, and the rebase onto #854 replaced it with the lenient-wrapper version.
   
   Minimum fix, which I ran both ways (fails on the swallowing mutant, passes 
on `3ab7498`):
   
   ```java
     @Test
     void apiErrorDuringVerificationIsPropagatedStrictly() {
       SparkAppContext context = buildContext(List.of(driverPodSpec), null);
       
when(context.getClient().pods().inNamespace("default").withName(anyString()).get())
           .thenThrow(new KubernetesClientException("boom", 500, null));
   
       Assertions.assertThrows(
           KubernetesClientException.class, 
context::getCurrentAttemptDriverPodStrictly);
     }
   ```
   
   Dropping the lenient wrapper instead gets this for free: the six behaviour 
cases retarget onto the strict method, and 
`apiErrorDuringVerificationIsTreatedAsAbsent` becomes the assertion above.
   



##########
spark-operator/src/main/java/org/apache/spark/k8s/operator/reconciler/reconcilesteps/AppInitStep.java:
##########
@@ -183,9 +184,36 @@ public ReconcileProgress reconcile(
    */
   private Optional<ReconcileProgress> holdForKueueAdmission(
       SparkAppContext context, SparkApplication app) {
-    if (!KueueWorkloadFactory.hasQueueName(app) || isDriverRequested(context)) 
{
+    if (!KueueWorkloadFactory.hasQueueName(app)) {
       return Optional.empty();
     }
+    try {
+      if (isDriverRequested(context)) {
+        return Optional.empty();
+      }
+    } catch (KubernetesClientException e) {
+      // Requesting the admission of a driver which is already running would 
be wrong, so the
+      // lookup is retried rather than failing the application with the 
terminal
+      // SchedulingFailure. Like a failed admission request, a transport level 
failure is not
+      // published, since writing an event would only add load to an API 
server that is often the
+      // cause of the failure, and it goes away on its own, so it keeps the 
short interval.
+      log.warn("Failed to check whether the driver exists before requesting 
admission.", e);
+      if (ReconcilerUtils.isTransientError(e)) {

Review Comment:
   **Finding 6.** This block, `ClusterInitStep.java:194-215` and 
`KueueWorkloadUtils.java:184-201` are now the same shape three times: 
`log.warn`, `isTransientError` to the short interval with no event, otherwise a 
`KueueAdmissionRequestFailed` warning and the default interval. Round 1 had one 
copy of it; this PR adds the second and third, which is the point at which it 
is worth naming.
   
   The risk is drift rather than volume. A future change to the classification 
— adding 429 to `isTransientError`, changing the persistent interval, adding a 
metric — has to be made in three places, and two of them are in files whose 
tests would not notice.
   
   Since all three live in front of `holdForAdmission` and share its reason, 
the helper belongs next to it:
   
   ```java
     /**
      * Reports a failed Kueue admission request, or a failed read that has to 
happen before one, and
      * returns the progress to retry it with. A transport level failure is not 
published, since
      * writing an event would only add load to an API server that is often the 
cause of it, and it
      * goes away on its own, so it keeps the short interval. Anything else, 
such as a missing RBAC
      * rule, is retried with the default interval so that its event is not 
rewritten every few
      * seconds until a user fixes the cause.
      */
     static ReconcileProgress retryAfterRequestFailure(
         final BaseContext<?> context, final KubernetesClientException e, final 
String what) {
       log.warn("{}, will retry.", what, e);
       if (ReconcilerUtils.isTransientError(e)) {
         return 
ReconcileProgress.completeAndRequeueAfter(STALE_WORKLOAD_REQUEUE_INTERVAL);
       }
       EventUtils.warn(
           context.getEventRecorder(),
           EventUtils.REASON_KUEUE_ADMISSION_REQUEST_FAILED,
           what + ", will retry. " + EventUtils.describe(e));
       return ReconcileProgress.completeAndDefaultRequeue();
     }
   ```
   
   Each catch then reads `return 
Optional.of(KueueWorkloadUtils.retryAfterRequestFailure(context, e, "Failed to 
check whether the driver exists before requesting Kueue admission"));`, and 
`holdForAdmission`'s own catch passes `"Failed to request Kueue admission"`. 
The five new tests already assert both arms for both steps, so they pin the 
extraction as it stands.
   



##########
spark-operator/src/main/java/org/apache/spark/k8s/operator/reconciler/reconcilesteps/ClusterInitStep.java:
##########
@@ -65,8 +67,20 @@ public ReconcileProgress reconcile(
     SparkCluster cluster = context.getResource();
     // A cluster whose master StatefulSet already exists has been requested 
before (e.g. the status
     // update to RunningHealthy failed), so let it complete its initialization 
even if suspended.
-    if (cluster.getSpec().isSuspend() && !isMasterRequested(context)) {
-      return SuspendUtils.holdForSuspend(context, "master and workers");
+    if (cluster.getSpec().isSuspend()) {
+      final boolean masterRequested;
+      try {
+        masterRequested = isMasterRequested(context);
+      } catch (KubernetesClientException e) {
+        // Whether the master is live is unknown, not answered. Holding would 
claim in an event
+        // that none was requested, and would release the Kueue quota of a 
running master, so
+        // look again with the steady-state interval instead.
+        log.warn("Failed to check whether the master of a suspended cluster 
exists.", e);
+        return completeAndDefaultRequeue();

Review Comment:
   **Finding 7.** Not publishing `SuspendHeld` here is right, since it would 
claim no master was requested. But publishing nothing leaves a suspended 
cluster with no signal at all when the failure is persistent, and 
`suspendedClusterWithUnverifiableMasterIsNotHeld` pins that silence with 
`verifyNoInteractions(eventRecorder)`.
   
   Three facts stack up. A suspended resource never gets its `Submitted` status 
persisted, which `docs/spark_custom_resources.md` states outright. This branch 
returns before `SuspendUtils.holdForSuspend`, so there is no event. And 
`verifyNoInteractions(recorder)` in that test confirms no status write. So for 
a 403 on `StatefulSet` reads the user sees an empty `Current State`, no events, 
and a 120-second loop that never resolves — while the operator cannot decide 
whether the master is live.
   
   For a 503 silence is the right answer, and for the same reason the admission 
branch gives: do not add event writes to an API server that is already failing. 
It is the persistent half that is asymmetric, because the admission branch 
fourteen lines down classifies the identical exception and reports it.
   
   `KueueAdmissionRequestFailed` does not fit, since no admission is involved, 
so this needs its own reason:
   
   ```java
         } catch (KubernetesClientException e) {
           log.warn("Failed to check whether the master of a suspended cluster 
exists.", e);
           if (!ReconcilerUtils.isTransientError(e)) {
             EventUtils.warn(
                 context.getEventRecorder(),
                 EventUtils.REASON_SUSPEND_CHECK_FAILED,
                 "Cannot tell whether the master was requested before, so the 
suspend hold is "
                     + "deferred. " + EventUtils.describe(e));
           }
           return completeAndDefaultRequeue();
         }
   ```
   
   plus the `REASON_SUSPEND_CHECK_FAILED` constant and a row in the `Warning` 
table of `docs/configuration.md`. `AppInitStep.java:78-84` has the same gap and 
came in with #854, so the same helper covers both. If you would rather not add 
a reason in this PR, a follow-up is fine — the cluster half is what this PR 
adds, so I am raising it here rather than against #854.
   



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