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


##########
docs/spark_custom_resources.md:
##########
@@ -586,6 +586,13 @@ spec:
   See [Optional Prerequisites](operations.md#optional-prerequisites).
 * The `Workload` is named `<lower-cased kind>-<resource name>` and is owned by 
the Spark resource,
   so it is garbage collected along with it.
+* Like Kueue built-in integrations, the `Workload` gets the priority of the 
`WorkloadPriorityClass`
+  named by the `kueue.x-k8s.io/priority-class` label. Without the label, the 
`priorityClassName` of
+  the driver (or master) pod template is used, then the one of the executor 
(or worker) pod
+  template, and then the `globalDefault` `PriorityClass`. The resource keeps 
waiting without a
+  `Workload` while the named priority class does not exist. Changing the label 
before the

Review Comment:
   **Finding 8.** "before the `Workload` is admitted" is now broader than what 
`isPriorityClassChangeAllowed` permits.
   
   Once quota is reserved but before admission, two label changes are silently 
dropped by the presence and group/kind freeze: adding the label to a `Workload` 
that has no ref at all, and adding it to one backed by a `globalDefault` 
`PriorityClass`. Both fall inside the window this sentence promises. The 
previous "while the `Workload` waits for quota" was accurate, so the new 
wording needs the exception spelled out:
   
   ```
     `Workload` while the named priority class does not exist. Changing the 
label before the
     `Workload` reserves quota updates its priority in place. After that Kueue 
freezes the
     presence, group and kind of the priority class, so only the name of a
     `WorkloadPriorityClass` still changes. A changed value of the same class 
does not affect
     the existing `Workload`.
   ```
   



##########
spark-operator/src/main/java/org/apache/spark/k8s/operator/kueue/KueueWorkloadUtils.java:
##########
@@ -124,9 +143,121 @@ public static AdmissionResult requestAdmission(
       deleteWorkload(client, workload);
       return AdmissionResult.STALE;
     }
+    WorkloadSpec spec = workload.getSpec();
+    PriorityClassRef desiredPriorityClassRef = 
desired.getSpec().getPriorityClassRef();
+    if (desired.getSpec().getPriority() != null
+        && !Objects.equals(spec.getPriorityClassRef(), desiredPriorityClassRef)
+        && isPriorityClassChangeAllowed(workload, desiredPriorityClassRef)) {
+      // Like Kueue, a changed priority class is applied in place so that the 
Workload keeps its
+      // position in the queue.
+      log.info(
+          "Updating the priority class of the pending Kueue Workload {}.",
+          workload.getMetadata().getName());
+      spec.setPriorityClassRef(desiredPriorityClassRef);
+      spec.setPriority(desired.getSpec().getPriority());
+      client.resource(workload).update();
+    }
     return AdmissionResult.PENDING;
   }
 
+  /**
+   * Checks whether Kueue accepts the priority class change of the given 
Workload. Like the Workload
+   * CEL rules, the presence, the group and the kind of the priorityClassRef 
are frozen once the
+   * quota is reserved, and so is the name of a Kubernetes PriorityClass. The 
name of a
+   * WorkloadPriorityClass stays mutable, which is what Kueue relies on to 
raise the priority of a
+   * Workload waiting for its admission checks.
+   */
+  private static boolean isPriorityClassChangeAllowed(
+      final Workload workload, final PriorityClassRef desired) {
+    if (workload.getStatus() == null || 
!workload.getStatus().isQuotaReserved()) {
+      return true;
+    }
+    PriorityClassRef current = workload.getSpec().getPriorityClassRef();
+    return current != null

Review Comment:
   **Finding 6.** Three of this helper's four conditions have no test, and 
every mutant survives the whole suite.
   
   I removed each in turn and ran `KueueWorkloadUtilsTest`:
   
   | removed | result |
   |---|---|
   | the `getGroup()` **and** `getKind()` equality checks | suite green |
   | `!SCHEDULING_API_GROUP.equals(desired.getGroup())` | suite green |
   | `current != null` | suite green |
   
   Group and kind are not separable, since the type's own CEL rule makes `group 
== 'scheduling.k8s.io'` imply `kind == 'PriorityClass'`, so either one alone 
still blocks. `quotaReservedWorkloadKeepsFrozenPriorityClass` only covers the 
presence freeze in the ref-to-none direction.
   
   All three are reachable. These three tests pass as written and kill all 
three mutants — I ran both arms:
   
   ```java
     @Test
     void quotaReservedWorkloadKeepsItsPodPriorityClassName() {
       createPriorityClass("default-a", 50, true);
       KueueWorkloadUtils.requestAdmission(kubernetesClient, 
workload("owner-uid-1", 1));
       reserveQuota();
       // The cluster-wide default changed, which is a PriorityClass name change
       kubernetesClient.resource(priorityClass("default-a", 50, 
false)).update();
       createPriorityClass("default-b", 70, true);
   
       Assertions.assertEquals(
           AdmissionResult.PENDING,
           KueueWorkloadUtils.requestAdmission(kubernetesClient, 
workload("owner-uid-1", 1)));
       Assertions.assertEquals("default-a", 
getWorkload().getSpec().getPriorityClassRef().getName());
       Assertions.assertEquals(50, getWorkload().getSpec().getPriority());
     }
   
     @Test
     void quotaReservedWorkloadDoesNotSwitchPriorityClassGroup() {
       createPriorityClass("default-a", 50, true);
       createWorkloadPriorityClass("high", 1000);
       KueueWorkloadUtils.requestAdmission(kubernetesClient, 
workload("owner-uid-1", 1));
       reserveQuota();
   
       // Adding the label would switch the group and the kind of the ref
       Assertions.assertEquals(
           AdmissionResult.PENDING,
           KueueWorkloadUtils.requestAdmission(kubernetesClient, 
workloadWithPriorityClass("high")));
       Assertions.assertEquals(
           "scheduling.k8s.io", 
getWorkload().getSpec().getPriorityClassRef().getGroup());
       Assertions.assertEquals(50, getWorkload().getSpec().getPriority());
     }
   
     @Test
     void quotaReservedWorkloadWithoutPriorityClassDoesNotGainOne() {
       createWorkloadPriorityClass("high", 1000);
       // The Workload is created while the operator cannot read the classes, 
so it has no ref
       KueueWorkloadUtils.requestAdmission(forbiddenClient(), 
workloadWithPriorityClass("high"));
       Assertions.assertNull(getWorkload().getSpec().getPriorityClassRef());
       reserveQuota();
   
       // The permission is back, but Kueue no longer accepts adding a priority 
class
       Assertions.assertEquals(
           AdmissionResult.PENDING,
           KueueWorkloadUtils.requestAdmission(kubernetesClient, 
workloadWithPriorityClass("high")));
       Assertions.assertNull(getWorkload().getSpec().getPriorityClassRef());
     }
   ```
   
   Two helpers to extract: `priorityClass(name, value, globalDefault)` out of 
`createPriorityClass`, and `forbiddenClient()` out of 
`pendingWorkloadKeepsItsPriorityWithoutPermission`.
   
   The last one matters most. Without `current != null` it does not merely 
allow a bad write, it throws `NullPointerException` out of `requestAdmission` 
on `current.getGroup()`. `holdForKueueAdmission` catches only 
`IllegalStateException` and `KubernetesClientException`, so the resource ends 
up in `SchedulingFailure`.
   
   One note on reachability while writing these: a pod template's 
`priorityClassName` change can never reach this helper, because it changes the 
pod sets hash and so takes the STALE/recreate path. A `scheduling.k8s.io` ref 
only changes name through a `globalDefault` swap, which is what the first test 
uses.
   



##########
spark-operator/src/main/java/org/apache/spark/k8s/operator/kueue/KueueWorkloadUtils.java:
##########
@@ -124,9 +143,121 @@ public static AdmissionResult requestAdmission(
       deleteWorkload(client, workload);
       return AdmissionResult.STALE;
     }
+    WorkloadSpec spec = workload.getSpec();
+    PriorityClassRef desiredPriorityClassRef = 
desired.getSpec().getPriorityClassRef();
+    if (desired.getSpec().getPriority() != null
+        && !Objects.equals(spec.getPriorityClassRef(), desiredPriorityClassRef)
+        && isPriorityClassChangeAllowed(workload, desiredPriorityClassRef)) {
+      // Like Kueue, a changed priority class is applied in place so that the 
Workload keeps its
+      // position in the queue.
+      log.info(
+          "Updating the priority class of the pending Kueue Workload {}.",
+          workload.getMetadata().getName());
+      spec.setPriorityClassRef(desiredPriorityClassRef);
+      spec.setPriority(desired.getSpec().getPriority());
+      client.resource(workload).update();
+    }
     return AdmissionResult.PENDING;
   }
 
+  /**
+   * Checks whether Kueue accepts the priority class change of the given 
Workload. Like the Workload
+   * CEL rules, the presence, the group and the kind of the priorityClassRef 
are frozen once the
+   * quota is reserved, and so is the name of a Kubernetes PriorityClass. The 
name of a
+   * WorkloadPriorityClass stays mutable, which is what Kueue relies on to 
raise the priority of a
+   * Workload waiting for its admission checks.
+   */
+  private static boolean isPriorityClassChangeAllowed(
+      final Workload workload, final PriorityClassRef desired) {
+    if (workload.getStatus() == null || 
!workload.getStatus().isQuotaReserved()) {
+      return true;
+    }
+    PriorityClassRef current = workload.getSpec().getPriorityClassRef();
+    return current != null
+        && desired != null
+        && Objects.equals(current.getGroup(), desired.getGroup())
+        && Objects.equals(current.getKind(), desired.getKind())
+        && !SCHEDULING_API_GROUP.equals(desired.getGroup());
+  }
+
+  /**
+   * Sets the priority of the desired Workload in the same way as Kueue 
built-in integrations. The
+   * WorkloadPriorityClass of the `kueue.x-k8s.io/priority-class` label takes 
precedence over the
+   * PriorityClass of the first pod set which has one, and the global default 
PriorityClass is used
+   * without both. Without any of them, the priority is 0 without a priority 
class. The priority is
+   * left unset if the operator is not allowed to read the cluster-scoped 
priority classes.
+   *
+   * @param client The KubernetesClient.
+   * @param workload The Workload whose priority is set in place.
+   * @throws IllegalStateException if the priority class does not exist.
+   */
+  static void setPriority(final KubernetesClient client, final Workload 
workload) {
+    WorkloadSpec spec = workload.getSpec();
+    try {
+      Map<String, String> labels = workload.getMetadata().getLabels();
+      String workloadPriorityClassName =
+          labels == null ? null : 
labels.get(Constants.LABEL_WORKLOAD_PRIORITY_CLASS);
+      if (StringUtils.isNotEmpty(workloadPriorityClassName)) {
+        WorkloadPriorityClass workloadPriorityClass =
+            
client.resources(WorkloadPriorityClass.class).withName(workloadPriorityClassName).get();
+        if (workloadPriorityClass == null) {
+          throw new IllegalStateException(
+              "Kueue WorkloadPriorityClass " + workloadPriorityClassName + " 
is not found.");
+        }
+        spec.setPriorityClassRef(
+            new PriorityClassRef(
+                Constants.KUEUE_API_GROUP, "WorkloadPriorityClass", 
workloadPriorityClassName));
+        spec.setPriority(workloadPriorityClass.getValue());
+        return;
+      }
+      PriorityClass priorityClass = getPodPriorityClass(client, spec);
+      if (priorityClass == null) {
+        spec.setPriorityClassRef(null);
+        spec.setPriority(DEFAULT_PRIORITY);
+      } else {
+        spec.setPriorityClassRef(
+            new PriorityClassRef(
+                SCHEDULING_API_GROUP, "PriorityClass", 
priorityClass.getMetadata().getName()));
+        spec.setPriority(priorityClass.getValue());
+      }
+    } catch (KubernetesClientException e) {
+      if (e.getCode() != HTTP_FORBIDDEN) {
+        throw e;
+      }
+      log.warn(
+          "Requesting the Kueue Workload {} without priority because the 
operator is not allowed "
+              + "to read the priority classes.",
+          workload.getMetadata().getName());
+    }
+  }
+
+  /**
+   * Returns the PriorityClass of the first pod set which has one, or the 
global default
+   * PriorityClass. Like Kueue, the lowest one wins if there are more than one 
global default.
+   */
+  private static PriorityClass getPodPriorityClass(
+      final KubernetesClient client, final WorkloadSpec spec) {
+    for (PodSet podSet : spec.getPodSets()) {
+      PodTemplateSpec template = podSet.getTemplate();
+      String name =
+          template == null || template.getSpec() == null
+              ? null
+              : template.getSpec().getPriorityClassName();
+      if (StringUtils.isNotEmpty(name)) {
+        PriorityClass priorityClass =
+            client.scheduling().v1().priorityClasses().withName(name).get();
+        if (priorityClass == null) {
+          throw new IllegalStateException("PriorityClass " + name + " is not 
found.");
+        }
+        return priorityClass;
+      }
+    }
+    return 
client.scheduling().v1().priorityClasses().list().getItems().stream()

Review Comment:
   **Finding 7.** This lookup runs on every reconcile, so a `Workload` with no 
label and no pod priority class gets re-priced whenever an admin changes the 
cluster's default. Kueue never does that.
   
   Observed on the mock server, with nothing about the Spark resource changed 
between the two reconciles:
   
   ```
   ref after create                   = 
PriorityClassRef(group=scheduling.k8s.io, kind=PriorityClass, name=default-a) 
priority=50
   ref after the globalDefault change = 
PriorityClassRef(group=scheduling.k8s.io, kind=PriorityClass, name=default-b) 
priority=70
   ```
   
   `classifyWorkloadsForPriorityUpdate` drops such a Workload from the update 
set outright:
   
   ```go
                if !workload.HasNoPriority(wl) && 
!workload.IsWorkloadPriorityClass(wl) {
                        continue
                }
   ```
   
   `HasNoPriority` is `PriorityClassRef == nil` and `IsWorkloadPriorityClass` 
requires the `kueue.x-k8s.io` group, so a `scheduling.k8s.io`-backed Workload 
never has its priority rewritten after creation. The comment above the function 
says it outright: "a Pod PriorityClass-backed workload does not follow the 
label at all". The effect here is that Spark `Workload`s would re-rank inside a 
`ClusterQueue` while every other workload in it holds still, one write each, 
which is the opposite of what this PR is trying to match.
   
   This also corrects the fix I sketched for finding 2. Kueue's gate has two 
parts and I only described the second:
   
   1. skip the Workload entirely when its ref is a Pod `PriorityClass`;
   2. otherwise compare the label to the ref's name, and resolve only when they 
differ.
   
   Part 1 is what fixes this finding, and it is also what makes part 2 
well-defined — without it a Pod-`PriorityClass`-backed Workload has no label to 
compare against, so a name comparison would resolve on every reconcile anyway.
   



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