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


##########
spark-operator/src/main/java/org/apache/spark/k8s/operator/utils/ReconcilerUtils.java:
##########
@@ -181,24 +187,54 @@ public static void addOwnerReferenceSecondaryResource(
   }
 
   /**
-   * Retrieves a Kubernetes resource by its desired state.
+   * Retrieves a Kubernetes resource by its desired state, reporting a 
resource that could not be
+   * read as absent.
    *
    * @param client The KubernetesClient.
    * @param desired The desired state of the resource.
    * @param <T> The type of the resource, extending HasMetadata.
-   * @return An Optional containing the retrieved resource, or empty if not 
found.
+   * @return An Optional containing the retrieved resource, or empty if not 
found or not readable.
    */
   public static <T extends HasMetadata> Optional<T> getResource(
       final KubernetesClient client, final T desired) {
-    T resource = null;
     try {
-      resource = client.resource(desired).get();
+      return getResourceStrictly(client, desired);
+    } catch (KubernetesClientException e) {
+      log.warn("Failed to read the resource with responseCode={}, considering 
it absent.",
+          e.getCode(), e);
+      return Optional.empty();
+    }
+  }
+
+  /**
+   * Retrieves a Kubernetes resource by its desired state, telling a missing 
resource apart from a
+   * read the API server refused. A transient failure keeps reporting the 
resource as absent, since
+   * the request did not reach a healthy API server and the create path, which 
re-reads on an
+   * AlreadyExists conflict, still resolves the actual state.
+   *
+   * @param client The KubernetesClient.
+   * @param desired The desired state of the resource.
+   * @param <T> The type of the resource, extending HasMetadata.
+   * @return An Optional containing the retrieved resource, or empty if not 
found or not reachable.
+   * @throws KubernetesClientException if the API server refused the read.
+   */
+  private static <T extends HasMetadata> Optional<T> getResourceStrictly(
+      final KubernetesClient client, final T desired) {
+    try {
+      return Optional.ofNullable(client.resource(desired).get());
     } catch (KubernetesClientException e) {
       if (e.getCode() == HTTP_NOT_FOUND) {
         return Optional.empty();
       }
+      if (isTransientError(e) || e.getCode() == HTTP_INTERNAL_ERROR) {

Review Comment:
   **Finding 1.** 429 lands in neither branch, so it falls through to `throw 
e`. That makes an API Priority and Fairness throttle fatal on a path this 
method otherwise treats as retryable.
   
   I drove `AppInitStep.reconcile` with a client whose driver-pod `get()` 
throws 429 and whose `create()` succeeds:
   
   ```
   main    9a61e23 : resulting state = DriverRequested
   head    7989aba : resulting state = SchedulingFailure
   ```
   
   `RestartConfig.restartPolicy` defaults to `Never` and 
`RestartPolicy.attemptRestartOnState(Never, …)` is unconditionally false, so 
that `SchedulingFailure` is the end of the application. All three `AppInitStep` 
call sites run inside the `catch (Exception e)` that writes it.
   
   The inconsistency is inside this same method. `getOrCreateSecondaryResource` 
has a dedicated 429 branch on the create side, and `shouldBackoffBeforeRetry` 
returns true for both 429 and a present `Retry-After`:
   
   ```java
             } else if (e.getCode() == Constants.HTTP_TOO_MANY_REQUESTS) {
               log.debug("Server returned 429 Too Many Requests, will retry 
with backoff");
   ```
   
   So the codebase already says 429 means "come back later". Your own javadoc 
frames the throwing bucket as "the API server refused the read", and a throttle 
is a deferral rather than a refusal, which is the same argument.
   
   `Constants` is already imported here, so it is one clause. I applied exactly 
this and re-ran the 429 arm, which then reaches `DriverRequested` like main:
   
   ```suggestion
         if (isTransientError(e)
             || e.getCode() == HTTP_INTERNAL_ERROR
             || e.getCode() == Constants.HTTP_TOO_MANY_REQUESTS) {
   ```
   
   `createsResourceWhenInitialReadDoesNotReachApiServer` should then take 429 
in its `@ValueSource`. Its name is already a slight stretch for 500, where the 
server did answer, and would be more so for 429 — something like 
`createsResourceWhenInitialReadGetsNoTrustworthyAnswer` would cover all three.
   



##########
spark-operator/src/main/java/org/apache/spark/k8s/operator/utils/ReconcilerUtils.java:
##########
@@ -181,24 +187,54 @@ public static void addOwnerReferenceSecondaryResource(
   }
 
   /**
-   * Retrieves a Kubernetes resource by its desired state.
+   * Retrieves a Kubernetes resource by its desired state, reporting a 
resource that could not be
+   * read as absent.
    *
    * @param client The KubernetesClient.
    * @param desired The desired state of the resource.
    * @param <T> The type of the resource, extending HasMetadata.
-   * @return An Optional containing the retrieved resource, or empty if not 
found.
+   * @return An Optional containing the retrieved resource, or empty if not 
found or not readable.
    */
   public static <T extends HasMetadata> Optional<T> getResource(
       final KubernetesClient client, final T desired) {
-    T resource = null;
     try {
-      resource = client.resource(desired).get();
+      return getResourceStrictly(client, desired);
+    } catch (KubernetesClientException e) {
+      log.warn("Failed to read the resource with responseCode={}, considering 
it absent.",
+          e.getCode(), e);
+      return Optional.empty();
+    }
+  }
+
+  /**
+   * Retrieves a Kubernetes resource by its desired state, telling a missing 
resource apart from a
+   * read the API server refused. A transient failure keeps reporting the 
resource as absent, since
+   * the request did not reach a healthy API server and the create path, which 
re-reads on an
+   * AlreadyExists conflict, still resolves the actual state.
+   *
+   * @param client The KubernetesClient.
+   * @param desired The desired state of the resource.
+   * @param <T> The type of the resource, extending HasMetadata.
+   * @return An Optional containing the retrieved resource, or empty if not 
found or not reachable.
+   * @throws KubernetesClientException if the API server refused the read.
+   */
+  private static <T extends HasMetadata> Optional<T> getResourceStrictly(

Review Comment:
   **Finding 2.** `private` leaves the one `getResource` caller outside this 
class with exactly the bug the description argues against.
   
   
`spark-operator/src/main/java/org/apache/spark/k8s/operator/reconciler/reconcilesteps/ClusterInitStep.java:208`
 is that caller, unchanged on main and on #847's head:
   
   ```java
     private boolean isMasterRequested(SparkClusterContext context) {
       return ReconcilerUtils.getResource(context.getClient(), 
context.getMasterStatefulSetSpec())
           .isPresent();
     }
   ```
   
   Both of its call sites read a refused read as "the master was never 
requested". The one that matters is the suspend branch at 
`ClusterInitStep.java:71`:
   
   ```java
       if (cluster.getSpec().isSuspend() && !isMasterRequested(context)) {
         ...
         if (KueueWorkloadFactory.hasQueueName(cluster)) {
           KueueWorkloadUtils.releaseWorkload(context.getClient(), cluster);
         }
   ```
   
   So a 403 on the `StatefulSet` read while a suspend is applied deletes the 
Kueue `Workload` of a cluster whose master is running, dropping quota that is 
still consumed. The Kueue-hold site at `ClusterInitStep.java:178` takes the 
same false branch but is idempotent, so the suspend one is the fix.
   
   `ReconcilerUtils` is in `…operator.utils` and `ClusterInitStep` in 
`…reconciler.reconcilesteps`, so package-private will not reach it — it needs 
`public`. `isMasterRequested` already runs inside `ClusterInitStep.reconcile`'s 
`try`, so a throw lands in the existing `SchedulingFailure` path with no new 
handling. A follow-up is fine if you would rather keep this PR to 
`ReconcilerUtils`, but the helper has to be reachable for that follow-up to 
exist.
   



##########
spark-operator/src/main/java/org/apache/spark/k8s/operator/utils/ReconcilerUtils.java:
##########
@@ -55,6 +55,7 @@
 
 /** Utility class for reconciler operations. */
 @Slf4j
+@SuppressWarnings("PMD.GodClass")

Review Comment:
   **Finding 3.** The premise checks out. I removed the annotation and ran 
`:spark-operator:pmdMain` on this head:
   
   ```
   ReconcilerUtils.java:58: GodClass: Possible God Class (WMC=50, ATFD=25, 
TCC=10.909%)
   ```
   
   The default threshold is WMC 47, so base sat at 47 and this PR takes it to 
50. The mechanism is fine — `GodClass` is class-scoped, there is nothing 
narrower to suppress — but the effect is that the rule is off for this class 
from here on, and the number is already moving in the wrong direction.
   
   `TCC=10.909%` is the part worth acting on: these methods barely share state 
because the class is four unrelated groups. `toUpdateControl`/`toDeleteControl` 
convert control objects, 
`getOrCreateSecondaryResource`/`getResource`/`getResourceStrictly`/`deleteResourceIfExists`
 are CRUD helpers, 
`isTransientError`/`shouldBackoffBeforeRetry`/`isFirstAttempt` classify 
failures, and `clone` is a JSON deep copy with nothing to do with reconciling — 
`ModelUtils` in `spark-operator-api` already owns the `objectMapper` it uses.
   
   I tried moving `clone` out to see whether that alone gets under the 
threshold and my quick edit tripped other PMD rules, so I am not claiming a 
specific refactor fixes the number. The ask is only that the suppression not be 
the permanent answer: a follow-up ticket to split the class, referenced from 
the annotation, keeps the signal alive.
   



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