docs: design doc for package execution as Jobs#307
Conversation
Design companion to #223: Job shape (one Job per skyhook/package/stage/node), completion flow with a persisted processed-once marker, outcome-based TTL, naming/rerun rules, upgrade dual-path, and rejected alternatives incl. podFailurePolicy for ImagePullBackOff (split to #306). Closes #299 Signed-off-by: Alex Yuskauskas <ayuskauskas@nvidia.com>
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds a design document describing migration of per-stage package execution from operator-managed Pods to one Kubernetes Job per skyhook, package, stage, and node. It specifies Job naming, labels, annotations, retry and completion behavior, deadline parking, suspension, outcome-based TTLs, idempotency, RBAC changes, legacy-pod compatibility during upgrades, and rejected alternatives. Estimated code review effort: 2 (Simple) | ~10 minutes Possibly related issues
🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 6
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@docs/designs/2026-07-10-package-execution-as-jobs.md`:
- Line 71: Update the quoted phrase in the explanation around the `pause`
container to use the hyphenated wording “in-flight” instead of “in flight.”
- Around line 48-49: The job metadata design currently uses the raw node name as
the skyhook.nvidia.com/node label value. Replace it with a bounded, stable hash
or truncated identifier that satisfies Kubernetes label limits, preserve the
full node name in an annotation, and update all related label/query examples and
references consistently.
- Around line 79-82: Make Job completion handling crash-safe and idempotent:
revise JobReconcile and the completion path around
HandleCompletePod/UpdateNodeState so processing is keyed by the Job UID or uses
a durable claim that is written before the non-idempotent state transition, with
recovery for claims left by crashes. Ensure duplicate or re-served
Complete/Failed events cannot repeat transitions or resurrect uninstall state,
while preserving the existing outcome TTL and special-case behavior.
- Around line 57-58: Update the Job/Pod lifecycle design to disambiguate Pods by
the current Job UID via controller owner references rather than relying only on
the job-name label. When multiple Pods exist, select the terminal Pod for the
correct attempt, and before recreating a deterministically reused Job name,
delete the old Job and wait until its owned Pods have fully disappeared,
including terminating Pods, before creation.
- Around line 57-58: Update the documented job configuration to account for
Kubernetes versions before v1.34: either raise the minimum supported version to
v1.34 or specify that podReplacementPolicy is conditionally omitted when the API
server lacks JobPodReplacementPolicy support.
- Around line 85-90: Correct the “Retry semantics” section to state that
restartPolicy: OnFailure retries contribute to Job backoff accounting and can
cause the Job to become Failed even when backoffLimit is 0. Update the described
recovery path accordingly, including how the operator handles a failed
crash-looping Job and whether it deletes and recreates it when the stage should
still run.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Enterprise
Run ID: e58dd2d4-bda5-4e4b-a67f-3c60cad508c9
📒 Files selected for processing (1)
docs/designs/2026-07-10-package-execution-as-jobs.md
Review (Fable, xhigh effort) found 3 blockers + 5 majors; all addressed: - AlreadyExists is no longer blindly benign: GET; finished/mismatched Jobs are foreground-deleted + requeued, never Upserted as in_progress - ValidateRunningPackages checks scoped to unfinished Jobs; finished Jobs cleaned only by TTL or a precise rerun predicate (protects retention) - Upgrade window: JobExists/HasRunningPackages/validation are legacy-pod aware to prevent duplicate executors and premature interrupts - not-ready/unreachable NoExecute tolerations so slow reboots don't evict the pod and fail the Job (backoffLimit: 0) - Failed (disruption) Jobs no longer write erroring: silent re-execution, keeping DeploymentPolicy failure counting unchanged - Crash-window claim corrected + stage-progress re-processing guard - Child-pod fallback for GC'd pods; package annotation on pod template - HandleConfigUpdates added to blast radius (delete Job, not child pod) - Interrupt Job name formula kept; metrics dead-code note; namespace-scoped Jobs informer; node-label length fallback; podReplacementPolicy rationale Also: TTL knobs named as chart values controllerManager.manager.env.jobTtl* Signed-off-by: Alex Yuskauskas <ayuskauskas@nvidia.com>
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@docs/designs/2026-07-10-package-execution-as-jobs.md`:
- Around line 77-88: Update the documented NotReady and Unreachable tolerations
to use a bounded, configurable tolerationSeconds reboot grace period instead of
infinite toleration. Add an explicit recovery path for permanently unavailable
nodes so the active Job can terminate and be replaced or otherwise converge
despite backoffLimit: 0, and revise the surrounding comments to describe this
behavior.
- Around line 139-141: Resolve the contradiction between the rerun predicate and
AlreadyExists handling: in ApplyPackage and Interrupt, after GETting an existing
Job, preserve any Complete Job lacking the state-recorded marker and requeue its
completion processing instead of deleting it. Continue deleting only Failed Jobs
or Complete Jobs already marked state-recorded, while leaving unfinished
matching Jobs untouched.
- Around line 120-129: Clarify the failure-path behavior in the TTL
documentation: specify where JobReconcile assigns JOB_TTL_FAILED for Failed Jobs
that do not write state-recorded, and whether this occurs before any retention
or deletion action. Explicitly describe the ordering with immediate disruption
reruns, including whether rerun deletion can happen before the TTL update and
that TTL is only a backstop when the Job remains present.
- Line 105: Define how the GC fallback supplies the required containerName for
UpdateNodeState and HandleCompletePod: persist the completion container name in
Job metadata when creating or updating the Job, then read it alongside stage,
node, interrupt, and restart data when the child Pod is gone; alternatively
implement and document a deterministic stage-to-container mapping that preserves
uninstall-interrupt handling.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Enterprise
Run ID: 2f826982-3514-43a5-ae84-ab498ca3c49f
📒 Files selected for processing (1)
docs/designs/2026-07-10-package-execution-as-jobs.md
Adversarial design review — summary for reviewersAn independent high-effort review pass ran against the initial design ( Blockers (all fixed)
Majors (all fixed)
Minors
Verified correct (no action)Baseline behavior section (pause container, init-status completion detection + delete-on-success, crash-loop-forever retries, silent vanished-pod recreate, labels/annotations, |
…ndings The load-bearing correction (CodeRabbit, confirmed against Job controller semantics): with restartPolicy: OnFailure the Job controller counts the sum of container restarts (init containers included) toward backoffLimit and terminates the pod at the limit — backoffLimit: 0 would kill a package on its first step retry. backoffLimit is now effectively unlimited (math.MaxInt32); retry ownership stays with kubelet in-place restarts, pod loss self-heals via the Job controller's replacement pod (nodeName-pinned template), and Job Failed becomes a backstop-only branch. Also, from CodeRabbit + a second adversarial review pass: - AlreadyExists must never delete a Complete-but-unrecorded Job (requeue; JobReconcile owns unprocessed completions) - child pods selected by batch.kubernetes.io/controller-uid, not job-name - containerName GC-fallback defined (interrupt label determines it) - Failed-path marker/TTL write specified; node-NotFound completion handling - postcondition guard now enumerates the interrupt/ProgressSkipped case - HasRunningPackages defined over unfinished Jobs (pod-based reading would stall interrupts behind retained Succeeded pods for the TTL window) - fourth legacy accommodation: HandleConfigUpdates keeps direct deletion of legacy erroring pods during the upgrade window - orphaned-node sweep covers Jobs of deleted nodes regardless of status - uninstall retention carve-outs documented; podReplacementPolicy gate-off wording honest; generation-label note fixed; in-flight hyphenation Signed-off-by: Alex Yuskauskas <ayuskauskas@nvidia.com>
Review round 2 — CodeRabbit triage + independent re-review (
|
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@docs/designs/2026-07-10-package-execution-as-jobs.md`:
- Around line 60-66: Clarify the backoffLimit explanations in the affected
design sections to distinguish failed Pods from active Pods: state that failed
Pods are counted separately, while container restarts are summed only for Pods
currently in Pending or Running phases when restartPolicy is OnFailure, not
across every Pod belonging to the Job.
- Around line 67-76: Update the package execution design to avoid relying on the
Job controller’s TerminatingOrFailed fallback when podReplacementPolicy=Failed
is unavailable. Gate this configuration by cluster capability and use an
operator-managed wait-then-recreate flow that waits for the prior node-bound Pod
to terminate before creating its replacement, preserving exclusive hostPath
access.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Enterprise
Run ID: 385b1551-56c4-4e14-afac-e01c27010b1c
📒 Files selected for processing (1)
docs/designs/2026-07-10-package-execution-as-jobs.md
… log-tail snapshot Enhancement over today's model (per review discussion): a stage that runs past its deadline — hung, crash-looping, or unpullable — is failed and surfaced instead of churning or hanging invisibly forever. - New additive SCR field Package.stageTimeout (metav1.Duration, follows gracefulShutdown pattern) -> Job activeDeadlineSeconds; operator default JOB_STAGE_TIMEOUT (1h, chart controllerManager.manager.env.jobStageTimeout), "0" disables. The one CRD change in the migration; additive, no shim. - DeadlineExceeded is a first-class failure: state -> erroring (new signal for hung stages; DeploymentPolicy sees stuck nodes), Job parked as the marker that stops recreation until rerun/reset/config-update or JOB_TTL_FAILED expiry (deliberate slow-retry cadence). Rules 2/3 gain the park exception; other Failed reasons stay the silent backstop. - Log-tail snapshot: on FailureTarget (pods still terminating) the operator captures the stuck container's last ~16KiB via the pod-logs API into the skyhook.nvidia.com/last-logs Job annotation, so the parked tombstone stays debuggable after the deadline deletes the pod. Best-effort, never blocks the park path. Needs pods/log get RBAC + a client-go clientset seam in dal. - Honest caveat retained: full post-deadline container logs live in SKYHOOK_LOG_DIR host logs / log aggregation. - Rejected alternative documented: restartPolicy Never + small backoffLimit (retains failed pods but can't catch hangs, changes retry substrate). Signed-off-by: Alex Yuskauskas <ayuskauskas@nvidia.com>
Addition (
|
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@docs/designs/2026-07-10-package-execution-as-jobs.md`:
- Around line 67-71: Update the package execution job examples and stageTimeout
behavior text to convert nonzero stageTimeout durations to whole integer seconds
for activeDeadlineSeconds, and omit activeDeadlineSeconds entirely when
stageTimeout is explicitly 0 or unset. Ensure both documentation locations
consistently describe and demonstrate this mapping.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Enterprise
Run ID: 9f8a2159-d71f-437b-8580-a4e50f9fdeec
📒 Files selected for processing (1)
docs/designs/2026-07-10-package-execution-as-jobs.md
Second deliberate enhancement: skyhook.nvidia.com/pause now halts in-flight stages instead of letting them finish. UpdatePauseStatus sets spec.suspend on the Skyhook's unfinished Jobs (annotation stays the user-facing primitive; suspension is enforcement). SIGTERM mid-step is the same recovery shape as reboot/eviction — agent flag files skip completed steps on resume. Suspension clears/resets Job startTime, so the stage deadline stops ticking while paused and resets fresh on resume — closing the bad interaction where a paused-but-running stage could hit its deadline and park as erroring. Suspended Jobs stay unfinished for JobExists/validation; interrupts that already fired a reboot converge via the resource-id flag; legacy pods keep let-finish semantics during the upgrade window (CLI docs must note the version-dependent strength). disable is unchanged. Replaces the earlier 'Rejected: suspend as pause primitive' section — the no-checkpointing cost is now accepted deliberately. Signed-off-by: Alex Yuskauskas <ayuskauskas@nvidia.com>
Addition (
|
…attempt archive Reworks the retry substrate (maintainer direction + review pass 3): - Package/stage Jobs: restartPolicy Never — each attempt is a fresh pod; the operator prunes Failed pods keeping ONE full-log archive (newest Failed without DisruptionTarget, by creationTimestamp; normal deletion only). kubectl logs on the last real failure works during retries, past the deadline, and through a pause — superseding the 16KiB snapshot for genuinely-failing stages (snapshot remains for hangs and never-started containers, now also recording Waiting reason+message, e.g. ImagePullBackOff + registry error). - podFailurePolicy Ignore-on-DisruptionTarget: evictions/preemption/PodGC count nothing and stay silent. backoffLimit stays MaxInt32 (counts Failed pods under Never). - INTERRUPT Jobs keep OnFailure (no podFailurePolicy): a reboot interrupt kills its own pod by design; Never would mint a spurious failed attempt per successful reboot. - Erroring evidence guards: no DisruptionTarget, real terminal verdict (skip ContainerStatusUnknown + admission rejections), and DeletionTimestamp unset — pause suspension, rule deletions, sweeps and manual pod deletes stay silent (review pass 3 blocker). - Resume half of the pause cascade gets an explicit owner and ordering (after ValidateRunningPackages; invalidate stale suspended Jobs first). - New rule 5: REAPPLY_ON_REBOOT reset sweeps the node's Jobs regardless of status — pre-reboot completions must not land on reset state. - Honest deltas documented: hard-crash 137/Error can flap erroring once; admission-rejected pods count attempts (replaces a worse latent wedge where such a raw pod satisfies PodExists forever); attempts figure now job.status.failed (user-visible nodeState/CLI improvement); deadline on an unreachable node surfaces via stale FailureTarget as erroring; stageTimeout "0" omits activeDeadlineSeconds; HandleConfigUpdates scoped to unfinished/parked Jobs; Complete path reads the Succeeded pod. Signed-off-by: Alex Yuskauskas <ayuskauskas@nvidia.com>
Rework (
|
|
Overall this is a strong, thorough design and I could follow the substance. My one piece of feedback is about readability: the detail isn't the problem, the layering is. Almost every sentence does three jobs at once (states the decision, justifies it, and pre-empts a reviewer objection), so there's no altitude a casual reader can cruise at. It reads a bit like the two adversarial review passes got inlined into the prose instead of layered underneath it. Some concrete ways to cut it down without losing the rigor: 1. Add a ~10-line TL;DR up top, before "Problems"A reader is dropped straight into full detail with no map. A short decision table would let someone get the shape in 60 seconds and choose where to drill in:
Everything after becomes optional depth rather than required reading. 2. Cut implementation identifiers out of the narrativeThis is the biggest single lever. The prose is dense with things that belong in code review, not a design doc:
A design doc can say "effectively unlimited backoff" and "a small byte cap (annotations share a per-object metadata budget)"; the exact number is a PR detail that will drift. Referencing behavior by role instead of by symbol would shrink the Retry, Completion, and Finished-Job sections noticeably and stop the reader context-switching between "design" and "codebase" every sentence. 3. Trim the parentheticals and inline
|
Address maintainer review (@lockwobr): the substance was approved but the layering buried it. Restructure without cutting the analysis: - add a TL;DR decision table up top so the shape reads in 60 seconds - describe behavior by role in the narrative; move Go symbols and file references to a baseline section and a new References block - drop exact constants and most inline cross-references from the prose - collapse the defensive material (crash-window guards, hard-crash deltas, admission edge, erroring guards, pruner safety) into an 'Edge cases and correctness arguments' appendix - collapse Rejected alternatives to a table, keeping only the central Never-vs-OnFailure decision in full Also fixes the meta-lint failure: the Goals list had a duplicate '4.' (MD029/ol-prefix); it is now sequential 1-5. Signed-off-by: Alex Yuskauskas <ayuskauskas@nvidia.com>
|
Restructured in
Also fixed the |
First PR of the #223 series (closes #299). Targets
feature/package-as-jobs, notmain— the feature branch PRs tomainonce the whole series lands.Adds
docs/designs/2026-07-10-package-execution-as-jobs.md: onebatch/v1Job per (skyhook, package, version, stage, node) replaces operator-managed raw Pods. Docs-only change; no code.Key decisions specified in the doc:
pausemain container becomes an exit-0 container so the pod can reachSucceededand the Job can complete. Newnot-ready/unreachableNoExecutetolerations (unbounded, deliberately) so reboot-class interrupts don't get the pod evicted at the 300s default.Never+ failed-attempt archive: package/stage Jobs userestartPolicy: Never(each attempt a fresh pod;backoffLimit: MaxInt32counts Failed pods;podFailurePolicyIgnore-on-DisruptionTargetkeeps disruptions silent and uncounted). The operator prunes to one archived Failed pod per Job — fullkubectl logson the last real failure through retries, deadline park, and pause. Interrupt Jobs keepOnFailure(a reboot interrupt kills its own pod by design). Erroring evidence is triple-guarded (no DisruptionTarget, real verdict, DeletionTimestamp unset) so suspension/sweeps/manual deletes stay silent.stageTimeoutfield (metav1.Duration,gracefulShutdownpattern) → JobactiveDeadlineSeconds; operator defaultJOB_STAGE_TIMEOUT(1h, chartcontrollerManager.manager.env.jobStageTimeout),"0"disables. A hung/crash-looping/unpullable stage now surfaces aserroringafter the deadline and parks — the Failed Job blocks recreation until rerun/reset/config-update orJOB_TTL_FAILEDexpiry (deliberate slow-retry cadence). OnFailureTargetthe operator snapshots the stuck container's last ~16KiB into theskyhook.nvidia.com/last-logsJob annotation before the pod is deleted, so the parked tombstone stays debuggable; full logs remain on the host (SKYHOOK_LOG_DIR).skyhook.nvidia.com/state-recordedJob annotation replaces delete-on-success as the processed-once marker, with per-transition postcondition guards (incl. the interrupt/ProgressSkippedcase) for the two-object crash window. Child pods are selected bybatch.kubernetes.io/controller-uid; a labels-based fallback covers GC'd pods; node-NotFound completions are marked processed instead of error-looping.ttlSecondsAfterFinishedunset; set at completion —JOB_TTL_SUCCEEDED(1h) /JOB_TTL_FAILED(24h), chart valuescontrollerManager.manager.env.jobTtlSucceeded/jobTtlFailed.AlreadyExistshandling that never discards unrecorded completions and honors the park; an orphaned-node sweep) — plusHasRunningPackages/JobExistscounting only unfinished Jobs.zz.migrationshim; four legacy accommodations for one minor, keyed on thebatch.kubernetes.io/job-namelabel (completion path, existence gating, validation sweep,HandleConfigUpdatesdirect pod deletion).pods/logget (log-tail snapshot), mirrored into the chart.podFailurePolicyfor ImagePullBackOff (cannot work — precise fix stays Surface ImagePullBackOff/ErrImagePull on package pods as state: erroring #306, though the deadline now bounds it),suspendas pause, separate JobReconciler, JobSet, static TTL,backoffLimit: 0variants,Never-mode parking, random name suffixes.Review history: two independent adversarial review passes plus CodeRabbit.
c5df70feaddressed pass 1 (3 blockers/5 majors);4f7e02a6addressed CodeRabbit (incl. thebackoffLimit/OnFailureaccounting correction) and pass 2 (4 majors);2653abcdadds the stage-deadline/park/log-snapshot enhancement from maintainer review discussion. Dispositions in the comments below.