peter-toth commented on code in PR #843:
URL:
https://github.com/apache/spark-kubernetes-operator/pull/843#discussion_r4038881565
##########
spark-operator/src/main/java/org/apache/spark/k8s/operator/reconciler/reconcilesteps/AppInitStep.java:
##########
@@ -100,6 +103,20 @@ public ReconcileProgress reconcile(
}
}
try {
+ // Like the suspend hold, a driver requested before must not be left
unobserved.
+ if (KueueWorkloadFactory.hasQueueName(app) &&
!isDriverRequested(context)) {
Review Comment:
**Finding 1.** This block is below the suspend gate at `:73`, and
`buildWorkload` has no other caller in `src/main`. Two consequences.
First, `KueueWorkloadFactory.java:186` computes `.active(!suspend)`, but
`suspend` is always `false` by the time it runs, so nothing ever writes
`active: false`. That wiring is unreachable today.
Second, and the reason this matters: suspending a resource that is *already*
queued does not release its quota. `getOrCreateSecondaryResource` only creates,
never updates, and `releaseWorkload` is called from `AppCleanUpStep` alone,
which a `Submitted` resource never reaches. So the `Workload` sits there with
`active: true`, Kueue admits it, and the quota is consumed by an application
the operator is deliberately refusing to start.
Observed at `a038938` with the mock client, reconciling once, then setting
`suspend: true` and reconciling again:
```
### r1 (not suspended) -> ReconcileProgress(completed=true, requeue=true,
requeueAfterDuration=PT2M)
### after r1: workload=true active=true
### r2 (suspended) -> ReconcileProgress(completed=true, requeue=true,
requeueAfterDuration=PT2M)
### after r2: workload=true active=true
```
It also makes `docs/spark_custom_resources.md:602` ("A suspended resource
does not get a `Workload` at all") true only when `suspend` was set before the
first admission request.
The smallest fix, which I applied and re-ran:
```java
if (app.getSpec().isSuspend() && !isDriverRequested(context)) {
log.debug("Application is suspended, driver resources would not be
requested.");
if (KueueWorkloadFactory.hasQueueName(app)) {
// A resource suspended while queued must not keep holding the Kueue
quota.
KueueWorkloadUtils.releaseWorkload(context.getClient(), app);
}
return completeAndDefaultRequeue();
}
```
`after r2` then reads `workload=false`. Note this needs one change in your
own new test: `suspendedAppWithQueueNameDoesNotCreateKueueWorkload` asserts
`verify(mockContext, never()).getClient()` and never stubs it, so it NPEs at
`KueueWorkloadUtils:142`. Stub the client and drop that `never()` line; the
`assertNull(getWorkload())` it already has is the assertion that matters.
`ClusterInitStep.java:68` needs the same treatment.
The better shape, if you want `active` to earn its keep, is to move the
admission block above the suspend gate and have `requestAdmission` reconcile
`spec.active` on an existing `Workload`. That keeps the queue position across a
suspend and is what Kueue's `active` field is for. It is more work, and
deleting is enough to stop the leak.
##########
spark-operator/src/test/java/org/apache/spark/k8s/operator/reconciler/reconcilesteps/AppInitStepTest.java:
##########
@@ -598,4 +612,182 @@ void staleInformerSnapshotDoesNotBypassSuspend() {
ApplicationStateSummary.ScheduledToRestart,
application.getStatus().getCurrentState().getCurrentStateSummary());
}
+
+ @Test
+ void kueueWorkloadIsCreatedAndDriverIsHeldUntilAdmitted() {
+ AppInitStep appInitStep = new AppInitStep();
+ SparkAppContext mockContext = mock(SparkAppContext.class);
+ SparkAppStatusRecorder recorder = mock(SparkAppStatusRecorder.class);
+ SparkApplication application = new SparkApplication();
+ application.setMetadata(kueueApplicationMetadata);
+ when(mockContext.getResource()).thenReturn(application);
+ when(mockContext.getClient()).thenReturn(kubernetesClient);
+
+ ReconcileProgress progress = appInitStep.reconcile(mockContext, recorder);
+
+ Assertions.assertEquals(ReconcileProgress.completeAndDefaultRequeue(),
progress);
+ Workload workload = getWorkload();
+ Assertions.assertNotNull(workload);
+ Assertions.assertEquals("test-queue", workload.getSpec().getQueueName());
+ Assertions.assertTrue(workload.getSpec().getActive());
+ verify(mockContext, never()).getDriverPodSpec();
+ verifyNoInteractions(recorder);
+ Assertions.assertEquals(
+ ApplicationStateSummary.Submitted,
+ application.getStatus().getCurrentState().getCurrentStateSummary());
+ }
+
+ @Test
+ void admittedKueueWorkloadRequestsDriver() {
+ AppInitStep appInitStep = new AppInitStep();
+ SparkAppContext mockContext = mock(SparkAppContext.class);
+ SparkAppStatusRecorder recorder = mock(SparkAppStatusRecorder.class);
+ SparkApplication application = new SparkApplication();
+ application.setMetadata(kueueApplicationMetadata);
+ when(mockContext.getResource()).thenReturn(application);
+ when(mockContext.getClient()).thenReturn(kubernetesClient);
+ 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;
+ });
+
+ // Not admitted yet: the driver is not requested
+ Assertions.assertEquals(
+ ReconcileProgress.completeAndDefaultRequeue(),
+ appInitStep.reconcile(mockContext, recorder));
+ Assertions.assertNull(
+
kubernetesClient.pods().inNamespace("default").withName("driver-pod").get());
+
+ admitWorkload();
+
+ Assertions.assertEquals(
+ ReconcileProgress.completeAndDefaultRequeue(),
+ appInitStep.reconcile(mockContext, recorder));
+ Assertions.assertNotNull(
+
kubernetesClient.pods().inNamespace("default").withName("driver-pod").get());
+ Assertions.assertEquals(
+ ApplicationStateSummary.DriverRequested,
+ application.getStatus().getCurrentState().getCurrentStateSummary());
+ }
+
+ @Test
+ void suspendedAppWithQueueNameDoesNotCreateKueueWorkload() {
+ AppInitStep appInitStep = new AppInitStep();
+ SparkAppContext mockContext = mock(SparkAppContext.class);
+ SparkAppStatusRecorder recorder = mock(SparkAppStatusRecorder.class);
+ SparkApplication application = new SparkApplication();
+ application.setMetadata(kueueApplicationMetadata);
+ application.getSpec().setSuspend(true);
+ when(mockContext.getResource()).thenReturn(application);
+
+ ReconcileProgress progress = appInitStep.reconcile(mockContext, recorder);
+
+ Assertions.assertEquals(ReconcileProgress.completeAndDefaultRequeue(),
progress);
+ Assertions.assertNull(getWorkload());
+ verify(mockContext, never()).getClient();
+ verifyNoInteractions(recorder);
+ }
+
+ @Test
+ void staleKueueWorkloadIsDeletedBeforeRequestingAdmission() {
+ AppInitStep appInitStep = new AppInitStep();
+ SparkAppContext mockContext = mock(SparkAppContext.class);
+ SparkAppStatusRecorder recorder = mock(SparkAppStatusRecorder.class);
+ SparkApplication application = new SparkApplication();
+ application.setMetadata(kueueApplicationMetadata);
+ when(mockContext.getResource()).thenReturn(application);
+ when(mockContext.getClient()).thenReturn(kubernetesClient);
+ // A Workload of a deleted application that had the same name is not
garbage collected yet
+ Workload stale = KueueWorkloadFactory.buildWorkload(application);
+ stale.getMetadata().getOwnerReferences().get(0).setUid("stale-uid");
+ kubernetesClient.resource(stale).create();
+
+ ReconcileProgress progress = appInitStep.reconcile(mockContext, recorder);
+
+ Assertions.assertEquals(
+ ReconcileProgress.completeAndRequeueAfter(
+ KueueWorkloadUtils.STALE_WORKLOAD_REQUEUE_INTERVAL),
+ progress);
+ Assertions.assertNull(getWorkload());
+ verify(mockContext, never()).getDriverPodSpec();
+ verifyNoInteractions(recorder);
+ }
+
+ @Test
+ void unsupportedKueueSpecFailsScheduling() {
+ AppInitStep appInitStep = new AppInitStep();
+ SparkAppContext mockContext = mock(SparkAppContext.class);
+ SparkAppStatusRecorder recorder = mock(SparkAppStatusRecorder.class);
+ SparkApplication application = new SparkApplication();
+ application.setMetadata(kueueApplicationMetadata);
+
application.getSpec().getSparkConf().put("spark.dynamicAllocation.enabled",
"true");
+ when(mockContext.getResource()).thenReturn(application);
+ when(mockContext.getClient()).thenReturn(kubernetesClient);
+
+ ReconcileProgress progress = appInitStep.reconcile(mockContext, recorder);
+
+ Assertions.assertEquals(ReconcileProgress.completeAndImmediateRequeue(),
progress);
+ Assertions.assertNull(getWorkload());
+ ArgumentCaptor<ApplicationStatus> captor =
ArgumentCaptor.forClass(ApplicationStatus.class);
+ verify(recorder).persistStatus(any(), captor.capture());
+ Assertions.assertEquals(
+ ApplicationStateSummary.SchedulingFailure,
+ captor.getValue().getCurrentState().getCurrentStateSummary());
Review Comment:
**Finding 2.** This test passes on `d848ee2`, where none of the Kueue code
exists. `getDriverPreResourcesSpec()` is left unstubbed, so the reconcile NPEs
on the very next line and the same `catch` writes the same `SchedulingFailure`
with the same `completeAndImmediateRequeue()`. `assertNull(getWorkload())`
holds too, because base never builds one.
Both messages, printed from the captured status:
```
head: Failed to request driver from scheduler backend. StackTrace:
java.lang.UnsupportedOperationException: Kueue does not support
SparkApplication with dynamic allocation (spark.dynamicAllocation.enabled=true)
yet.
base d848ee2: Failed to request driver from scheduler backend. StackTrace:
java.lang.NullPointerException: Cannot invoke "Object.getClass()" because
"meta" is null
```
Asserting the message closes it. I ran this on both refs: it passes at head
and fails on base.
```suggestion
ApplicationStateSummary.SchedulingFailure,
captor.getValue().getCurrentState().getCurrentStateSummary());
Assertions.assertTrue(
captor.getValue().getCurrentState().getMessage().contains("dynamic
allocation"),
captor.getValue().getCurrentState().getMessage());
```
The other three that pass on base are fine as they are:
`suspendedAppWithQueueNameDoesNotCreateKueueWorkload`,
`driverRequestedBeforeBypassesKueueAdmission` and the cluster's
`masterRequestedBeforeBypassesKueueAdmission` pin an *ordering*, so they fail
on the mutation that matters (moving the check above the gate) even though base
has no check to move.
##########
spark-operator/src/main/java/org/apache/spark/k8s/operator/reconciler/reconcilesteps/ClusterInitStep.java:
##########
@@ -77,6 +78,19 @@ public ReconcileProgress reconcile(
}
}
try {
+ // Like the suspend hold, a master requested before must complete its
initialization.
+ if (KueueWorkloadFactory.hasQueueName(cluster) &&
!isMasterRequested(context)) {
+ AdmissionResult admission =
+ KueueWorkloadUtils.requestAdmission(
+ context.getClient(),
KueueWorkloadFactory.buildWorkload(cluster));
Review Comment:
**Finding 3.** `requestAdmission` throws for reasons that have nothing to do
with the spec: `IllegalStateException` when the `Workload` can neither be read
nor created, and a `KubernetesClientException` propagated from the
stale-`Workload` delete, which it deliberately does not swallow "so that the
resource is not created until the stale Workload is gone" (documented at
`KueueWorkloadUtils.java:80-86`, and see
[r4029146888](https://github.com/apache/spark-kubernetes-operator/pull/835#discussion_r4029146888)).
Inside this `try`, both become `SchedulingFailure`. For a `SparkCluster`
that is the end: `SparkClusterReconciler.getReconcileSteps` maps
`SchedulingFailure` to `ClusterUnknownStateStep`, which appends `Failed`
unconditionally, and `ClusterInitStep` is never selected again. Observed with a
client whose `Workload` calls fail:
```
### progress = ReconcileProgress(completed=true, requeue=true,
requeueAfterDuration=PT0S)
### state = SchedulingFailure
### message = Failed to request Spark cluster from scheduler backend.
StackTrace: java.lang.IllegalStateException: Failed to request Kueue Workload
with name: sparkcluster-cluster1
```
So a `SparkCluster` submitted while the operator is missing the Kueue RBAC
verb, or while Kueue is briefly unreachable, is permanently dead and has to be
recreated. A `SparkApplication` at least gets the restart policy.
Note that `ReconcilerUtils.getResource` swallows every non-404
`KubernetesClientException` and reports the resource as absent, so a 403 on the
GET reaches this path as "create it", which is what produced the message above.
The gate is not resource creation, so it wants its own error handling: keep
`buildWorkload` inside the `try` so an unsupported spec still lands in
`SchedulingFailure`, and treat an API failure as a retry.
```java
if (KueueWorkloadFactory.hasQueueName(cluster) &&
!isMasterRequested(context)) {
Workload desired = KueueWorkloadFactory.buildWorkload(cluster);
AdmissionResult admission;
try {
admission =
KueueWorkloadUtils.requestAdmission(context.getClient(), desired);
} catch (IllegalStateException | KubernetesClientException e) {
log.warn("Failed to request Kueue admission, will retry.", e);
return
completeAndRequeueAfter(KueueWorkloadUtils.STALE_WORKLOAD_REQUEUE_INTERVAL);
}
...
```
`AppInitStep.java:108` has the same shape.
##########
docs/spark_custom_resources.md:
##########
@@ -557,9 +557,52 @@ spec:
application is configured to restart, the next attempt is held until
`suspend` is set back to
`false`. Setting it to `true` on a running cluster has no effect in the
current version.
* Deleting a suspended resource works as usual.
-* This is the building block for external job queueing systems such as
- [Kueue](https://kueue.sigs.k8s.io/), which admit a workload by flipping
`suspend` to `false`.
- The operator does not integrate with such a system yet.
+* This is the building block for external job queueing systems. See
[Kueue](#kueue) for the
+ built-in integration.
+
+## Kueue
+
+A `SparkApplication` or a `SparkCluster` labeled with
`kueue.x-k8s.io/queue-name` is queued by
+[Kueue](https://kueue.sigs.k8s.io/). The operator creates a Kueue `Workload`
that describes the
+driver and executor (or master and worker) pod sets, and holds the creation of
those resources
+until Kueue admits the `Workload`.
+
+```yaml
+apiVersion: spark.apache.org/v1
+kind: SparkApplication
+metadata:
+ name: spark-pi
+ labels:
+ kueue.x-k8s.io/queue-name: spark-queue
+spec:
+ mainClass: "org.apache.spark.examples.SparkPi"
+ jars: "local:///opt/spark/examples/jars/spark-examples.jar"
+ runtimeVersions:
+ sparkVersion: "4.2.0"
+```
+
+* The label alone enables the integration. Kueue and its `LocalQueue` must
exist, and the operator
+ needs the Kueue RBAC rules which the Helm chart grants when
`operatorRbac.kueue.enabled` is set.
+ 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.
+* While the `Workload` waits for quota, the resource stays in its initializing
state (`Submitted`,
+ or `ScheduledToRestart` for a restarted attempt) and no driver (or master /
worker) is created.
+ Like `spec.suspend`, the initial `Submitted` status of the first attempt is
not persisted to the
+ API server, so `kubectl get` shows an empty `Current State` and no events
are published until the
+ `Workload` is admitted. Use `kubectl get workload` to see the admission
status. If the spec
+ changes while waiting, the `Workload` is recreated with the new resource
requests.
+* When a `SparkApplication` attempt stops and its resources are released, the
operator deletes the
+ `Workload` so that Kueue releases the quota. A restarted attempt is queued
again. Resources
+ retained by `resourceRetainPolicy` keep the quota until they are released.
+* A `SparkCluster` requests the resources set on the `master` and `worker`
containers of its pod
+ templates. A missing request defaults to the limit, or else to 1 CPU and
`SPARK_DAEMON_MEMORY`
+ plus overhead. A worker uses `SPARK_WORKER_CORES` for the CPU and adds
`SPARK_WORKER_MEMORY` to
+ the memory when they are set. A `SparkCluster` keeps the quota until it is
deleted.
+* `spec.suspend` takes precedence. A suspended resource does not get a
`Workload` at all.
+* Dynamic allocation, a `SparkCluster` with `minWorkers < maxWorkers`, and pod
template files set
+ through `spark.kubernetes.{driver,executor}.podTemplateFile` are not
supported yet. Such a
+ resource fails with `SchedulingFailure` instead of being queued.
Review Comment:
**Finding 4.** The list is missing the limitation a Kueue user is most
likely to assume works. `isAdmissionChanged` is symmetric, so an admitted
`Workload` that stops being admitted does fire a reconciliation — the operator
is woken and then throws the signal away. `AppInitStep.reconcile` returns
`proceed()` at `:69` once the state is past initializing, the Kueue block is
additionally skipped by `isDriverRequested`, and no other step or observer
reads `Workload` status. So when Kueue evicts — preemption by a higher
`WorkloadPriorityClass`, `spec.active` set to `false`, the `ClusterQueue`
stopped — it releases the quota and the driver and executors keep running. Real
usage then exceeds the quota with nothing to correct it.
This was left open deliberately at the `KueueWorkloadUtils` stage, on the
grounds that `AdmissionResult` cannot express "was admitted, then evicted" and
that deciding it needs an observer rather than the init steps. This PR is the
one that wires admission into reconciliation and the one that adds the wake-up,
so it is the right place to state the gap even if the fix is a follow-up.
```suggestion
resource fails with `SchedulingFailure` instead of being queued.
* Preemption is not honored yet. The operator checks the admission only
before it creates the
resources, so a later eviction (or `spec.active` set to `false` on the
`Workload`) releases the
Kueue quota while the driver and executors, or the master and workers,
keep running.
```
##########
tests/e2e/kueue/spark-example-queued.yaml:
##########
@@ -0,0 +1,37 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+apiVersion: spark.apache.org/v1
+kind: SparkApplication
+metadata:
+ name: spark-job-kueue-queued-test
+ namespace: default
+ labels:
+ kueue.x-k8s.io/queue-name: ($LOCAL_QUEUE)
+spec:
+ mainClass: "org.apache.spark.examples.SparkPi"
+ jars: "local:///opt/spark/examples/jars/spark-examples.jar"
+ # The Kueue admission takes one more reconciliation, which uses up the
per-resource rate limit
+ # (5 reconciliations per 15 seconds) before the driver starts. Run longer
than that window so
+ # that the driver transitions are observed after the rate limit is refreshed.
+ driverArgs: ["10000"]
Review Comment:
**Finding 6.** The comment is right, and the numbers check out:
`maxLoopForPeriod` is 5 and `refreshPeriodSeconds` is 15
(`docs/config_properties.md:50-51`). But what it describes is a property of
every queued resource, not of this test. The reconciliation that creates the
`Workload` and returns `PENDING` is a token a non-queued application never
spends, so a short queued job reaches the limiter one event earlier and its
terminal transition can be observed up to a refresh period late.
Making SparkPi run 10000 iterations moves the e2e past the window, which is
a reasonable way to keep the test deterministic. It also means the suite no
longer covers the case the comment is about. Two things worth doing:
- Say it in the user-facing section, next to the sentence about the
`Workload` informer, so an operator running short queued jobs is not surprised
by a late `Succeeded`:
```
* A queued resource spends one reconciliation on the admission itself, so
with the default
rate limiter (5 loops per 15s) a short application may report its
terminal state up to one
refresh period later than an unqueued one.
```
- Reword the comment here to say the test is avoiding the window rather than
implying the cost is a test artifact.
The `withOnAddFilter(workload -> false)` /
`withOnUpdateFilter(isAdmissionChanged)` pair already removes the larger part
of this. I checked that the filters do not affect the informer's cache:
`InformerEventSource.onAddOrUpdate` updates `PrimaryToSecondaryIndex` and
`TemporaryResourceCache` before it consults either filter, so only event
propagation is suppressed.
##########
docs/spark_custom_resources.md:
##########
@@ -557,9 +557,52 @@ spec:
application is configured to restart, the next attempt is held until
`suspend` is set back to
`false`. Setting it to `true` on a running cluster has no effect in the
current version.
* Deleting a suspended resource works as usual.
-* This is the building block for external job queueing systems such as
- [Kueue](https://kueue.sigs.k8s.io/), which admit a workload by flipping
`suspend` to `false`.
- The operator does not integrate with such a system yet.
+* This is the building block for external job queueing systems. See
[Kueue](#kueue) for the
+ built-in integration.
+
+## Kueue
+
+A `SparkApplication` or a `SparkCluster` labeled with
`kueue.x-k8s.io/queue-name` is queued by
+[Kueue](https://kueue.sigs.k8s.io/). The operator creates a Kueue `Workload`
that describes the
+driver and executor (or master and worker) pod sets, and holds the creation of
those resources
+until Kueue admits the `Workload`.
+
+```yaml
+apiVersion: spark.apache.org/v1
+kind: SparkApplication
+metadata:
+ name: spark-pi
+ labels:
+ kueue.x-k8s.io/queue-name: spark-queue
+spec:
+ mainClass: "org.apache.spark.examples.SparkPi"
+ jars: "local:///opt/spark/examples/jars/spark-examples.jar"
+ runtimeVersions:
+ sparkVersion: "4.2.0"
+```
+
+* The label alone enables the integration. Kueue and its `LocalQueue` must
exist, and the operator
+ needs the Kueue RBAC rules which the Helm chart grants when
`operatorRbac.kueue.enabled` is set.
+ 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.
+* While the `Workload` waits for quota, the resource stays in its initializing
state (`Submitted`,
+ or `ScheduledToRestart` for a restarted attempt) and no driver (or master /
worker) is created.
+ Like `spec.suspend`, the initial `Submitted` status of the first attempt is
not persisted to the
+ API server, so `kubectl get` shows an empty `Current State` and no events
are published until the
+ `Workload` is admitted. Use `kubectl get workload` to see the admission
status. If the spec
+ changes while waiting, the `Workload` is recreated with the new resource
requests.
+* When a `SparkApplication` attempt stops and its resources are released, the
operator deletes the
+ `Workload` so that Kueue releases the quota. A restarted attempt is queued
again. Resources
+ retained by `resourceRetainPolicy` keep the quota until they are released.
+* A `SparkCluster` requests the resources set on the `master` and `worker`
containers of its pod
+ templates. A missing request defaults to the limit, or else to 1 CPU and
`SPARK_DAEMON_MEMORY`
+ plus overhead. A worker uses `SPARK_WORKER_CORES` for the CPU and adds
`SPARK_WORKER_MEMORY` to
+ the memory when they are set. A `SparkCluster` keeps the quota until it is
deleted.
Review Comment:
**Finding 5.** Everything here is accurate about what the operator computes,
which is what makes it misleading as guidance: it does not say that the
fallback is a floor rather than the pod's footprint.
`WorkerArguments.inferDefaultCores()` is
`Runtime.getRuntime.availableProcessors()` and `inferDefaultMemory()` is
`max(totalMb - 1024, 1024)`, and with no cpu or memory limit on the container
there is no cgroup bound for the JVM to read. So a worker with neither
`SPARK_WORKER_CORES` nor a CPU limit advertises the whole node to its executors
while the `Workload` asks for 1 CPU.
This is the finding I left on the PR that added the calculation
([r4036382704](https://github.com/apache/spark-kubernetes-operator/pull/841#discussion_r4036382704)),
and this section is the first user-facing place it can be said.
```suggestion
the memory when they are set. A `SparkCluster` keeps the quota until it is
deleted. Set a cpu and
memory request or limit on the `worker` container, or `SPARK_WORKER_CORES`
and
`SPARK_WORKER_MEMORY`: a worker with none of them advertises the whole
node to its executors,
well above the default the `Workload` requests.
```
--
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]