dongjoon-hyun commented on code in PR #909:
URL:
https://github.com/apache/spark-kubernetes-operator/pull/909#discussion_r4146679904
##########
spark-operator/src/main/java/org/apache/spark/k8s/operator/reconciler/reconcilesteps/ClusterInitStep.java:
##########
@@ -68,27 +68,30 @@ public ReconcileProgress reconcile(
return proceed();
}
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.
+
+ boolean masterRequested = false;
if (cluster.getSpec().isSuspend()) {
- final boolean masterRequested;
+ // Unlike a first attempt, a cluster resumed from Suspended or queued
again after an eviction
+ // has persisted this Submitted state, which would keep saying that it
is resumed. It goes
+ // back to Suspended instead, where ClusterSuspendStep deletes the
master and worker
+ // StatefulSets by name and releases whatever it holds only after its
pods are gone. That
+ // happens whether or not the master was requested, so it is not looked
up.
+ if (cluster.getStatus().getStateTransitionHistory().lastKey() > 0) {
+ return appendStateAndImmediateRequeue(
+ context, statusRecorder, new ClusterState(Suspended,
CLUSTER_SUSPENDED_MESSAGE));
+ }
+ // 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.
Review Comment:
**[P1]** The new comment above says that `ClusterSuspendStep` deletes the
master and worker `StatefulSet`s by name and releases the `Workload` only after
the pods are gone, whether or not the master was requested. That holds for a
first attempt whose master exists as well, yet such a cluster still completes
its initialization here. It applies every resource again and persists
`RunningHealthy`, and only then does `ClusterSuspendStep` move it to
`Suspended` and delete what was just applied. For example, suppose the previous
reconciliation applied the master `StatefulSet` but failed to apply the worker
one with a retryable error, or the status update to `RunningHealthy` failed,
and `spec.suspend` is set to `true` afterwards.
- The worker `StatefulSet`, which never existed, is created, and its pods
start only to be deleted in the next reconciliation.
- If an admission webhook keeps failing the `NetworkPolicy`, HPA or PDB with
500, only `ClusterRequestFailed` repeats. The suspension is never applied,
while the master and its Kueue `Workload` keep the quota.
- A non-retryable failure, e.g. 422 for a spec edited while suspended, or a
node selector conflict of the Kueue flavors ends the cluster in
`SchedulingFailure`, although the user only suspended it.
Could we move such a cluster to `Suspended` as well, like the `lastKey() >
0` branch? Then `holdForKueueAdmission` is reached only when `spec.suspend` is
`false`, so the new `masterRequested` parameter and both `masterRequested ||`
are no longer needed. `suspendAfterMasterRequestedCompletesInitialization` and
the new `suspendedQueuedClusterWithRequestedMasterIsLookedUpOnce`, which assert
`RunningHealthy` for this case, would change accordingly. Since this path
predates this PR, a follow-up JIRA issue is also fine.
##########
spark-operator/src/main/java/org/apache/spark/k8s/operator/reconciler/reconcilesteps/ClusterInitStep.java:
##########
@@ -221,19 +225,22 @@ private ReconcileProgress failScheduling(
*
* @param context The SparkClusterContext for the cluster.
* @param cluster The SparkCluster.
+ * @param masterRequested Whether the master was found requested already in
this reconcile, e.g.
+ * by the suspend check, so that it is not looked up again.
* @return The progress to return while the admission is not granted, or
empty to proceed.
*/
private Optional<ReconcileProgress> holdForKueueAdmission(
- SparkClusterContext context, SparkCluster cluster) {
+ SparkClusterContext context, SparkCluster cluster, boolean
masterRequested) {
try {
if (!KueueWorkloadFactory.hasQueueName(cluster)) {
- return KueueWorkloadUtils.handleDequeuedWorkload(context, () ->
isMasterRequested(context));
+ return KueueWorkloadUtils.handleDequeuedWorkload(
+ context, () -> masterRequested || isMasterRequested(context));
Review Comment:
**[P2]** No test covers this `masterRequested ||`. With this line reverted
to `() -> isMasterRequested(context)`, all 555 tests of `:spark-operator:test`
still pass.
- `suspendedQueuedClusterWithRequestedMasterIsLookedUpOnce` uses a cluster
with the queue label, so it reaches only L243.
- `kueueFlavorsAreAppliedAgainWhenTheMasterExists(queueLabelRemoved=true)`
is not suspended, so `masterRequested` is `false` there.
- `suspendAfterMasterRequestedCompletesInitialization` doesn't stub
`getCachedKueueWorkload()`, so `handleDequeuedWorkload` returns before it
evaluates the supplier.
The uncovered case is a suspended first attempt whose queue label was
removed after its admission. Without this line, a 403 of the second read is
reported as `KueueAdmissionRequestFailed` (`... before requesting Kueue
admission`), which is what the comment of the new test wants to avoid. A `null`
applies the `StatefulSet`s again without the flavors of the admitted
`Workload`. P9 suggests a way to cover both L237 and L243.
##########
spark-operator/src/test/java/org/apache/spark/k8s/operator/reconciler/reconcilesteps/ClusterInitStepTest.java:
##########
@@ -405,6 +408,61 @@ void suspendAfterMasterRequestedCompletesInitialization() {
verify(statefulSetApplicable, times(2)).serverSideApply();
}
+ @Test
+ @SuppressWarnings("unchecked")
+ void suspendedQueuedClusterWithRequestedMasterIsLookedUpOnce() {
+ // The suspend check already found the master, so the Kueue admission must
not read it again:
+ // a failure of that second read would be reported as
KueueAdmissionRequestFailed rather than
+ // SuspendCheckFailed, and a null would request the admission of a running
master
+ ClusterInitStep clusterInitStep = new ClusterInitStep();
+ SparkClusterContext mockContext = mock(SparkClusterContext.class);
+ SparkClusterStatusRecorder recorder =
mock(SparkClusterStatusRecorder.class);
+ SparkCluster cluster = buildKueueCluster();
+ cluster.getSpec().setSuspend(true);
+ KubernetesClient mockClient = mock(KubernetesClient.class,
RETURNS_DEEP_STUBS);
+ when(mockClient.resource(masterStatefulSetSpec).get())
+ .thenReturn(masterStatefulSetSpec)
+ .thenThrow(new KubernetesClientException("forbidden", 403, null));
+ stubWorkloadRead(mockClient, admittedWorkload(cluster, Map.of()));
+ ServerSideApplicable<Service> serviceApplicable =
mock(ServerSideApplicable.class);
+ ServiceResource<Service> serviceResource = mock(ServiceResource.class);
+ when(serviceResource.forceConflicts()).thenReturn(serviceApplicable);
+
when(mockClient.services().resource(any(Service.class))).thenReturn(serviceResource);
+ ServerSideApplicable<StatefulSet> statefulSetApplicable =
mock(ServerSideApplicable.class);
+ RollableScalableResource<StatefulSet> statefulSetResource =
+ mock(RollableScalableResource.class);
+
when(statefulSetResource.forceConflicts()).thenReturn(statefulSetApplicable);
+ when(mockClient.apps().statefulSets().resource(any(StatefulSet.class)))
+ .thenReturn(statefulSetResource);
+ ServerSideApplicable<NetworkPolicy> networkPolicyApplicable =
mock(ServerSideApplicable.class);
+ Resource<NetworkPolicy> networkPolicyResource = mock(Resource.class);
+
when(networkPolicyResource.forceConflicts()).thenReturn(networkPolicyApplicable);
+
when(mockClient.network().networkPolicies().resource(any(NetworkPolicy.class)))
+ .thenReturn(networkPolicyResource);
+ when(mockContext.getResource()).thenReturn(cluster);
+ when(mockContext.getClient()).thenReturn(mockClient);
+
when(mockContext.getMasterServiceSpec()).thenReturn(service("cluster1-master-svc"));
+
when(mockContext.getWorkerServiceSpec()).thenReturn(service("cluster1-worker-svc"));
+
when(mockContext.getMasterStatefulSetSpec()).thenReturn(masterStatefulSetSpec);
+
when(mockContext.getWorkerStatefulSetSpec()).thenReturn(workerStatefulSetSpec);
+
when(mockContext.getWorkerNetworkPolicySpec()).thenReturn(networkPolicy("cluster1-worker"));
+
when(mockContext.getHorizontalPodAutoscalerSpec()).thenReturn(Optional.empty());
+
when(mockContext.getPodDisruptionBudgetSpec()).thenReturn(Optional.empty());
+ when(mockContext.getEventRecorder()).thenReturn(eventRecorder);
+
+ ReconcileProgress progress = clusterInitStep.reconcile(mockContext,
recorder);
+
+ Assertions.assertEquals(ReconcileProgress.completeAndDefaultRequeue(),
progress);
+ verify(mockClient.resource(masterStatefulSetSpec)).get();
+ verify(mockClient, never()).resource(any(Workload.class));
Review Comment:
**[P3]** Two of these assertions cannot fail for the regression which this
test targets. With L243 of `ClusterInitStep` reverted to `if
(isMasterRequested(context))`, the second `get()` throws the stubbed 403, which
`holdForKueueAdmission` turns into `retryAfterRequestFailure`. It returns
`completeAndDefaultRequeue()` as well, and nothing reaches
`client.resource(workload)`. So only L456, L458 and L460 catch it, and the test
passes with L455 and L457 alone.
The `null` case in the comment (`a null would request the admission of a
running master`) is not exercised either. Even with `.thenReturn(null)` as the
second answer, the test fails earlier, since the unstubbed
`priorityClasses().list()` in `KueueWorkloadPriority` throws a
`NullPointerException`, which ends in `SchedulingFailure`. How about saying in
the comment that the `get()` count at L456 guards this, and either dropping
L457 or covering the `null` case with `priorityClasses().list()` stubbed as
well?
##########
docs/spark_custom_resources.md:
##########
@@ -544,7 +544,12 @@ published when enabled. Since the status is not there to
fall back on, it is rep
minutes by default while the hold lasts, so that it outlives the event
retention of the API
server. An application held later, in `ScheduledToRestart`, keeps the status
its previous attempt
already wrote and gets the same event, since that status says that a restart
is due, not that the
-next attempt is withheld.
+next attempt is withheld. If the driver (or master) cannot be read to check
whether it was requested
+already, e.g. since the API server rejects the read, the resource is neither
held nor started, and
+the `SuspendCheckFailed` event is published instead, until the check succeeds.
A failure which may
+clear on its own, such as a timeout, is retried without the event. Once the
check succeeds again,
+the `SuspendHeld` event may follow only with its next repeat, so a
`SuspendCheckFailed` event which
+is no longer refreshed no longer applies.
Review Comment:
**[P4]** This doesn't hold once the check fails only at the transport level,
and neither does `One which is no longer refreshed has cleared.` in
`configuration.md`. For example, the master read fails with 500, and
`SuspendCheckFailed` is published. If the same outage then continues with 503,
504 or connection resets, `retryAfterCheckFailure` skips the event but still
neither holds nor starts the cluster, so its pending `Workload` is not released
either. A user who follows this sentence would conclude that the failure is
over.
```suggestion
the `SuspendHeld` event may follow only with its next repeat, so a
`SuspendCheckFailed` event which
is no longer refreshed no longer applies, unless the check now fails only at
the transport level.
```
##########
spark-operator/src/main/java/org/apache/spark/k8s/operator/reconciler/reconcilesteps/ClusterInitStep.java:
##########
@@ -68,27 +68,30 @@ public ReconcileProgress reconcile(
return proceed();
}
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.
+
+ boolean masterRequested = false;
if (cluster.getSpec().isSuspend()) {
- final boolean masterRequested;
+ // Unlike a first attempt, a cluster resumed from Suspended or queued
again after an eviction
+ // has persisted this Submitted state, which would keep saying that it
is resumed. It goes
+ // back to Suspended instead, where ClusterSuspendStep deletes the
master and worker
+ // StatefulSets by name and releases whatever it holds only after its
pods are gone. That
+ // happens whether or not the master was requested, so it is not looked
up.
+ if (cluster.getStatus().getStateTransitionHistory().lastKey() > 0) {
+ return appendStateAndImmediateRequeue(
+ context, statusRecorder, new ClusterState(Suspended,
CLUSTER_SUSPENDED_MESSAGE));
+ }
Review Comment:
**[P8]** This changes a behavior which the PR description calls unchanged
(`The requeue behavior is unchanged: ...`). A suspended `SparkCluster` resumed
from `Suspended` or queued again after an eviction now goes back to `Suspended`
without the lookup. Before, a failed lookup was retried in `Submitted` with the
default interval, and an existing master completed the initialization to
`RunningHealthy` first. Could you update the PR description, including the
user-facing change section? It becomes the commit message when merged.
##########
docs/spark_custom_resources.md:
##########
@@ -544,7 +544,12 @@ published when enabled. Since the status is not there to
fall back on, it is rep
minutes by default while the hold lasts, so that it outlives the event
retention of the API
server. An application held later, in `ScheduledToRestart`, keeps the status
its previous attempt
already wrote and gets the same event, since that status says that a restart
is due, not that the
-next attempt is withheld.
+next attempt is withheld. If the driver (or master) cannot be read to check
whether it was requested
+already, e.g. since the API server rejects the read, the resource is neither
held nor started, and
+the `SuspendCheckFailed` event is published instead, until the check succeeds.
A failure which may
Review Comment:
**[P6]** Events are disabled by default
(`spark.kubernetes.operator.events.enabled=false`), and this document says
`when enabled` for the other events, e.g. `SuspendHeld` at L542-543. With the
defaults, a suspended first attempt whose check fails has neither a persisted
status nor this event, only the operator log.
```suggestion
the `SuspendCheckFailed` event is published instead when enabled, until the
check succeeds. A failure which may
```
##########
spark-operator/src/main/java/org/apache/spark/k8s/operator/reconciler/reconcilesteps/ClusterInitStep.java:
##########
@@ -68,27 +68,30 @@ public ReconcileProgress reconcile(
return proceed();
}
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.
+
+ boolean masterRequested = false;
if (cluster.getSpec().isSuspend()) {
- final boolean masterRequested;
+ // Unlike a first attempt, a cluster resumed from Suspended or queued
again after an eviction
+ // has persisted this Submitted state, which would keep saying that it
is resumed. It goes
+ // back to Suspended instead, where ClusterSuspendStep deletes the
master and worker
+ // StatefulSets by name and releases whatever it holds only after its
pods are gone. That
+ // happens whether or not the master was requested, so it is not looked
up.
Review Comment:
**[P7]** nit. A cluster queued again after an eviction persisted `Submitted`
with `CLUSTER_REQUEUED_MESSAGE` (`Cluster is submitted again after its eviction
by Kueue.`), not with the resumed one, see `String message = evicted ?
CLUSTER_REQUEUED_MESSAGE : CLUSTER_RESUMED_MESSAGE;` in `ClusterSuspendStep`.
```suggestion
// Unlike a first attempt, a cluster resumed from Suspended or queued
again after an eviction
// has persisted this Submitted state, which would keep saying that it
is resumed or queued
// again. It goes back to Suspended instead, where ClusterSuspendStep
deletes the master and
// worker StatefulSets by name and releases whatever it holds only
after its pods are gone.
// That happens whether or not the master was requested, so it is not
looked up.
```
##########
docs/spark_custom_resources.md:
##########
@@ -544,7 +544,12 @@ published when enabled. Since the status is not there to
fall back on, it is rep
minutes by default while the hold lasts, so that it outlives the event
retention of the API
server. An application held later, in `ScheduledToRestart`, keeps the status
its previous attempt
already wrote and gets the same event, since that status says that a restart
is due, not that the
-next attempt is withheld.
+next attempt is withheld. If the driver (or master) cannot be read to check
whether it was requested
+already, e.g. since the API server rejects the read, the resource is neither
held nor started, and
Review Comment:
**[P12]** The Kueue section still says that `suspending a queued resource
deletes its Workload to release the quota` (L691), and the guide for disabling
the integration asks to suspend the queued resources `so that the operator
releases their Workloads itself` (L720-722). With this paragraph, that no
longer holds while the check fails. For example, if the read keeps failing with
403, the `Workload` stays until the read succeeds, and a pending one may even
be admitted meanwhile. The behavior predates this PR, but now that it is
documented here, how about qualifying those two places too, e.g. by linking to
this section?
##########
spark-operator/src/test/java/org/apache/spark/k8s/operator/reconciler/reconcilesteps/ClusterInitStepTest.java:
##########
@@ -321,17 +321,20 @@ void resumedClusterSuspendedAgainGoesBackToSuspended() {
new ClusterState(ClusterStateSummary.Submitted,
Constants.CLUSTER_RESUMED_MESSAGE),
true));
cluster.getSpec().setSuspend(true);
+ // Not stubbed: a read would find a master, but the cluster must not look
it up at all
KubernetesClient mockClient = mock(KubernetesClient.class,
RETURNS_DEEP_STUBS);
- when(mockClient.resource(masterStatefulSetSpec).get()).thenReturn(null);
when(mockContext.getResource()).thenReturn(cluster);
when(mockContext.getClient()).thenReturn(mockClient);
when(mockContext.getMasterStatefulSetSpec()).thenReturn(masterStatefulSetSpec);
Review Comment:
**[P10]** nit. This stub is no longer used, since the cluster now goes to
`Suspended` before the lookup, and it contradicts `verify(mockContext,
never()).getMasterStatefulSetSpec()` at L336. It passes only because these
tests don't use strict stubs.
```suggestion
```
##########
spark-operator/src/main/java/org/apache/spark/k8s/operator/reconciler/reconcilesteps/SuspendUtils.java:
##########
@@ -88,4 +91,37 @@ static ReconcileProgress holdForSuspend(final BaseContext<?>
context, final Stri
return completeAndRequeueAfter(
Duration.ofSeconds(SUSPEND_HOLD_REQUEUE_INTERVAL_SECONDS.getValue()));
}
+
+ /**
+ * Reports a failed check of whether the resources of a suspended resource
were requested, and
+ * returns the progress to retry it with. Whether they are live is unknown
then, so the resource
+ * is neither held, which would claim in an event that none was requested
and release the Kueue
+ * quota of a running driver or master, nor started. Like a failed Kueue
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 it. Anything else is, since a suspended
resource may have no
+ * persisted status to show it and would otherwise be retried without any
signal.
+ *
+ * @param context The context of the suspended resource.
+ * @param e The failure to report.
+ * @param requested The resources whose request could not be checked, as
named in the event and
+ * the log, e.g. {@code "driver"}.
+ * @return The progress to return, requeued after the steady-state interval.
+ */
+ static ReconcileProgress retryAfterCheckFailure(
+ final BaseContext<?> context, final KubernetesClientException e, final
String requested) {
+ String what =
+ "Failed to check whether the "
+ + requested
+ + " of the suspended "
+ + context.getResource().getKind()
+ + " was requested";
+ log.warn("{}, will retry.", what, e);
+ if (!ReconcilerUtils.isTransientError(e)) {
+ EventUtils.warn(
+ context.getEventRecorder(),
+ EventUtils.REASON_SUSPEND_CHECK_FAILED,
+ what + ", will retry. " + EventUtils.describe(e));
+ }
+ return completeAndDefaultRequeue();
Review Comment:
**[P14]** nit. Except for the reason and the interval of a transient
failure, this is the same as `KueueWorkloadUtils.retryAfterRequestFailure`: the
same log line, the same `what + ", will retry. " + EventUtils.describe(e)` and
the same gate. On top of the current `main`, the `log.warn` plus `if
(!ReconcilerUtils.isTransientError(e)) EventUtils.warn(...)` pattern appears in
seven places with this PR, e.g. `ClusterSuspendStep`, `ClusterInitStep`
(`ClusterRequestFailed`), `StatusRecorder` and
`KueueWorkloadUtils.recordPodsReady`. A helper like
`EventUtils.warnUnlessTransient(recorder, reason, message, e)` would keep the
policy in one place. This can be a follow-up.
##########
spark-operator/src/main/java/org/apache/spark/k8s/operator/reconciler/reconcilesteps/ClusterInitStep.java:
##########
@@ -68,27 +68,30 @@ public ReconcileProgress reconcile(
return proceed();
}
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.
+
+ boolean masterRequested = false;
if (cluster.getSpec().isSuspend()) {
- final boolean masterRequested;
+ // Unlike a first attempt, a cluster resumed from Suspended or queued
again after an eviction
+ // has persisted this Submitted state, which would keep saying that it
is resumed. It goes
+ // back to Suspended instead, where ClusterSuspendStep deletes the
master and worker
+ // StatefulSets by name and releases whatever it holds only after its
pods are gone. That
+ // happens whether or not the master was requested, so it is not looked
up.
+ if (cluster.getStatus().getStateTransitionHistory().lastKey() > 0) {
+ return appendStateAndImmediateRequeue(
+ context, statusRecorder, new ClusterState(Suspended,
CLUSTER_SUSPENDED_MESSAGE));
+ }
+ // 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.
try {
masterRequested = isMasterRequested(context);
} catch (KubernetesClientException e) {
Review Comment:
**[P5]** This catches only a `KubernetesClientException`, but
`isMasterRequested` builds the whole master `StatefulSet` spec only for its
name. If the spec cannot be built, e.g. after it was edited while suspended,
the exception leaves the reconciliation, since the `try` at L106 is not reached
yet. Then a suspended first attempt gets neither `SuspendHeld` nor
`SuspendCheckFailed`, only a `ReconcileError` while the JOSDK retries it, and
`holdForSuspend` never releases its pending `Workload`.
`AppInitStep` handles the same case with `hasLiveDriverPod`, and
`ClusterSuspendStep` deletes the `StatefulSet`s by name, since the spec `may
not even build after being edited while suspended`. How about reading the
master by name in `isMasterRequested` as well, i.e.
`apps().statefulSets().inNamespace(namespace).withName(getMasterStatefulSetName(name)).get()`?
This is not introduced by this PR, and I couldn't find a spec which passes the
validation and still fails to build, so a follow-up JIRA issue is fine.
##########
spark-operator/src/test/java/org/apache/spark/k8s/operator/reconciler/reconcilesteps/ClusterInitStepTest.java:
##########
@@ -321,17 +321,20 @@ void resumedClusterSuspendedAgainGoesBackToSuspended() {
new ClusterState(ClusterStateSummary.Submitted,
Constants.CLUSTER_RESUMED_MESSAGE),
Review Comment:
**[P11]** The new branches are tested only for this resumed history, a 503,
and an application in `Submitted`.
- A cluster queued again after an eviction (`CLUSTER_REQUEUED_MESSAGE`),
which the new comment names, is not tested. The branch doesn't read the message
today, but `ClusterSuspendStep` tells the two apart, so a parameter over both
messages would guard a later change.
- A transport-level failure, e.g. `new KubernetesClientException("closed",
new SocketException("reset"))`, is not tested for the suspend check of either
step. Only `ReconcilerUtilsTest` checks its classification.
- `SuspendCheckFailed` of an application in `ScheduledToRestart`, which the
new docs sentence follows, is not tested either.
##########
docs/configuration.md:
##########
@@ -93,6 +93,7 @@ In addition, the operator publishes the following `Warning`
events.
| `KueueAdmissionRequestFailed` | Creating, reading or deleting a stale Kueue
`Workload` fails, or the driver or master cannot be read before the admission
is requested. Transport-level errors are skipped and retried every 5 seconds,
while a persistent failure is retried with the default interval. |
| `ClusterRequestFailed` | Applying the Services, StatefulSets, NetworkPolicy,
HorizontalPodAutoscaler or PodDisruptionBudget of a `SparkCluster` fails in a
way which may yet succeed, such as a throttled request or an internal server
error from an admission webhook, so it is retried with the default interval
instead of failing the cluster. Transport-level errors are skipped. |
| `SuspendReleaseFailed` | Deleting the master and worker StatefulSets, the
HorizontalPodAutoscaler or PodDisruptionBudget of the workers, or the Kueue
`Workload` of a `SparkCluster` suspended while running fails, or its pods
cannot be listed. A failure which may clear on its own, such as a timeout, is
skipped. Every failure is retried with the default interval. |
+| `SuspendCheckFailed` | The driver of a suspended `SparkApplication`, or the
master of a suspended `SparkCluster`, cannot be read to check whether it was
requested, so the resource is neither held, which would release its Kueue
`Workload`, nor started. Transport-level errors are skipped. Every such failure
is retried with the default interval. While the failure lasts, it is
republished on every reconciliation, subject to
[`minIntervalSeconds`](#event-frequency). Once the check succeeds again, the
unchanged `SuspendHeld` event may be dropped by the same interval until its
next repeat, so this event can stay the newest one for up to 30 minutes
(`spark.kubernetes.operator.reconciler.suspendHoldRequeueIntervalSeconds`). One
which is no longer refreshed has cleared. |
Review Comment:
**[P13]** `up to 30 minutes` holds only while `minIntervalSeconds` is at
most `suspendHoldRequeueIntervalSeconds`. With `minIntervalSeconds=2400`, which
the Event frequency section uses as an example, the recovering `SuspendHeld` is
dropped at its next repeat as well, so this event stays the newest one for
about 60 minutes. The interval is also in the `SuspendHeld` row already. How
about `until its next repeat` only? The suggestion also rewords the last
sentence for P4.
```suggestion
| `SuspendCheckFailed` | The driver of a suspended `SparkApplication`, or
the master of a suspended `SparkCluster`, cannot be read to check whether it
was requested, so the resource is neither held, which would release its Kueue
`Workload`, nor started. Transport-level errors are skipped. Every such failure
is retried with the default interval. While the failure lasts, it is
republished on every reconciliation, subject to
[`minIntervalSeconds`](#event-frequency). Once the check succeeds again, the
unchanged `SuspendHeld` event may be dropped by the same interval until its
next repeat, so this event can stay the newest one until then. One which is no
longer refreshed has cleared, unless the check now fails only at the transport
level. |
```
##########
spark-operator/src/test/java/org/apache/spark/k8s/operator/reconciler/reconcilesteps/ClusterInitStepTest.java:
##########
@@ -405,6 +408,61 @@ void suspendAfterMasterRequestedCompletesInitialization() {
verify(statefulSetApplicable, times(2)).serverSideApply();
}
+ @Test
+ @SuppressWarnings("unchecked")
+ void suspendedQueuedClusterWithRequestedMasterIsLookedUpOnce() {
Review Comment:
**[P9]** This test copies the block of the apply and spec stubs, which this
file already has six times. How about a `suspended` parameter of
`kueueFlavorsAreAppliedAgainWhenTheMasterExists` instead? For example:
```java
@ParameterizedTest
@CsvSource({"false, false", "true, false", "false, true", "true, true"})
@SuppressWarnings("unchecked")
void kueueFlavorsAreAppliedAgainWhenTheMasterExists(
boolean queueLabelRemoved, boolean suspended) {
...
SparkCluster cluster = buildKueueCluster();
cluster.getSpec().setSuspend(suspended);
...
verify(mockClient.resource(masterStatefulSetSpec)).get();
```
I tried it locally. It passes with this PR, while it fails only for `(true,
true)` with L237 reverted and only for `(false, true)` with L243 reverted, so
it covers P2 as well.
##########
spark-operator/src/test/java/org/apache/spark/k8s/operator/reconciler/reconcilesteps/AppInitStepTest.java:
##########
@@ -562,6 +562,36 @@ void suspendedAppWithUnverifiableDriverIsNotHeld() {
application.getStatus().getCurrentState().getCurrentStateSummary());
}
+ @ParameterizedTest
+ @ValueSource(ints = {403, 429, 500})
+ void suspendedAppWithPersistentlyUnverifiableDriverPublishesEvent(int code) {
Review Comment:
**[P15]** nit. This test and
`suspendedClusterWithPersistentlyUnverifiableMasterPublishesEvent` differ from
the 503 tests right above them only in the code and the event assertions. They
could be one test each, like
`retryableFailureOfRequestingResourcesIsRetried(int code, int events)` in
`ClusterInitStepTest`, e.g. with `@CsvSource({"503, 0", "403, 1", "429, 1",
"500, 1"})`. Then the `Submitted` assertion of the 503 tests would cover the
other codes as well. The current split is also fine, since
`ClusterSuspendStepTest` does the same.
--
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]