Skip to content

Commit 83741d3

Browse files
committed
docs(operator): separate the retry budget from the parking decision
CodeRabbit review on #402. The comments conflated two decisions the code deliberately keeps apart: backoffLimit counts Failed pods, but exhausting it only takes the Job terminal — the stage parks as erroring solely when a retained attempt genuinely failed. A Failed pod falls into one of three classes: an ignored disruption spends nothing; an attempt the kubelet refused to admit spends an attempt without being the package's failure; a genuine step failure or per-attempt timeout spends an attempt and is. Only the third parks. Say so in the options docstring, manager.yaml, values.yaml, and the design doc's backoffLimit paragraph, which still claimed only genuine failures could count at all. Signed-off-by: Alex Yuskauskas <ayuskauskas@nvidia.com>
1 parent a31ccbe commit 83741d3

4 files changed

Lines changed: 13 additions & 8 deletions

File tree

chart/values.yaml

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -90,10 +90,11 @@ controllerManager:
9090
## as the only limit.
9191
jobStageTimeout: "1h"
9292
## jobBackoffLimit is how many retries a package stage gets after its first attempt before
93-
## its Job goes terminal and the stage parks as erroring, awaiting a rerun/reset/config
94-
## change — so a stage runs at most jobBackoffLimit+1 times. It bounds every failure class,
95-
## not just timeouts, so raising it buys a crash-looping package more time to self-heal.
96-
## "0" gives a stage a single attempt with no retry.
93+
## its Job goes terminal — so a stage runs at most jobBackoffLimit+1 times. If a retained
94+
## attempt genuinely failed the stage then parks as erroring, awaiting a rerun/reset/config
95+
## change; otherwise it simply re-runs. It bounds every failure class, not just timeouts,
96+
## so raising it buys a crash-looping package more time to self-heal. "0" gives a stage a
97+
## single attempt with no retry.
9798
jobBackoffLimit: "3"
9899
## runtimeRequiredTaint: This feature assumes nodes are added to the cluster with `--register-with-taints` kubelet flag.
99100
## This taint is assume to be all new nodes, and skyhook pods will tolerate this taint, and remove it one the nodes packages are complete.

docs/designs/2026-07-10-package-execution-as-jobs.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ Three things about this shape are non-obvious:
110110
- **`restartPolicy: Never` on package Jobs** makes each attempt a fresh pod, so a failure survives as a full-log archive pod instead of being destroyed by an in-place restart. Interrupt Jobs are the exception — they keep `OnFailure`, because a reboot interrupt kills its own pod by design; under `Never` every successful reboot would mint a spurious failed attempt, whereas in-place restart after the node returns is the proven recovery shape (the agent skips the already-done interrupt via its resource-id flag file).
111111
- **The unbounded not-ready/unreachable tolerations** stop the taint manager from evicting a node-pinned pod when a reboot-class interrupt keeps the node NotReady past the default eviction timeout. These pods are node-bound host agents — running them anywhere else is meaningless, so eviction is never useful. A node that stays NotReady forever holds the Job Active exactly as today's raw pod would; a *removed* node is handled by the orphaned-node sweep.
112112

113-
`backoffLimit` is finite (`JOB_BACKOFF_LIMIT`, default 3): under `restartPolicy: Never` it counts Failed pods, and the `Ignore`-on-`DisruptionTarget` rule means only *genuine* step failures count, not disruptions. The retry loop lives inside the Job controller — attempt fails → the operator archives the Failed pod → the Job controller paces the next attempt under its own backoff — so the operator never drives recreation, and there is no operator-side retry counter to persist. `podReplacementPolicy: Failed` guarantees the replacement lands only after the previous pod has fully terminated, so two executors never overlap on the shared hostPath mounts.
113+
`backoffLimit` is finite (`JOB_BACKOFF_LIMIT`, default 3): under `restartPolicy: Never` it counts Failed pods, and the `Ignore`-on-`DisruptionTarget` rule keeps disruptions out of that count. Counting and *parking* are then two different questions, and a Failed pod falls into one of three classes: an ignored disruption spends nothing; an attempt the kubelet refused to admit spends an attempt but is not the package's failure; a genuine step failure or per-attempt timeout spends an attempt and is. Exhausting the budget therefore takes the Job terminal, but only the third class parks the stage — see Retry and the failed-attempt archive. The retry loop lives inside the Job controller — attempt fails → the operator archives the Failed pod → the Job controller paces the next attempt under its own backoff — so the operator never drives recreation, and there is no operator-side retry counter to persist. `podReplacementPolicy: Failed` guarantees the replacement lands only after the previous pod has fully terminated, so two executors never overlap on the shared hostPath mounts.
114114

115115
Interrupt Jobs are the exception and keep an unbounded limit. Under `OnFailure` the limit counts container *restarts* rather than failed pods, so a finite budget would be spent by the in-place restart that *is* the reboot recovery.
116116

operator/config/manager/manager.yaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,8 @@ spec:
112112
# package-stage Jobs by outcome; JobStageTimeout is the default per-attempt
113113
# deadline when a package sets no stageTimeout (0 removes the time bound);
114114
# JobBackoffLimit is how many retries a package stage gets after its first attempt
115-
# before it parks, so a stage runs at most JobBackoffLimit+1 times.
115+
# before its Job goes terminal, so a stage runs at most JobBackoffLimit+1 times; it
116+
# parks as erroring only if a retained attempt genuinely failed.
116117
- name: JOB_TTL_SUCCEEDED
117118
value: "1h"
118119
- name: JOB_TTL_FAILED

operator/internal/controller/skyhook_controller.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -149,8 +149,11 @@ type JobOperatorOptions struct {
149149
// outcome so failure logs outlive success logs; JobStageTimeout is the default
150150
// per-attempt deadline for a package stage Job when the package sets no stageTimeout
151151
// (0 removes the time bound); JobBackoffLimit is how many *retries* a package stage gets
152-
// after its first attempt before the Job goes terminal and the stage parks, so the stage
153-
// runs at most JobBackoffLimit+1 times (0 means a single attempt, no retry).
152+
// after its first attempt before its Job goes terminal, so the stage runs at most
153+
// JobBackoffLimit+1 times (0 means a single attempt, no retry). Exhausting the budget is
154+
// not by itself a park: the stage parks as erroring only if a retained attempt genuinely
155+
// failed, since attempts the kubelet refused to admit spend the budget without ever
156+
// running the package.
154157
JobTTLSucceeded time.Duration `env:"JOB_TTL_SUCCEEDED, default=1h"`
155158
JobTTLFailed time.Duration `env:"JOB_TTL_FAILED, default=24h"`
156159
JobStageTimeout time.Duration `env:"JOB_STAGE_TIMEOUT, default=1h"`

0 commit comments

Comments
 (0)