Skip to content

fix(doctor): don't treat an assigned-but-Pending training pod as a running job (backend#3247) - #642

Merged
aptracebloc merged 2 commits into
developfrom
fix/3247-doctor-pending-not-running
Sep 7, 2026
Merged

fix(doctor): don't treat an assigned-but-Pending training pod as a running job (backend#3247)#642
aptracebloc merged 2 commits into
developfrom
fix/3247-doctor-pending-not-running

Conversation

@aptracebloc

@aptracebloc aptracebloc commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

What

checkNodeFit counted every non-terminal job-name pod that had a NodeName into the "a running job holds the room" sum — including pods still Pending (ImagePullBackOff / ContainerCreating). A training pod scheduled to a node but wedged on an image pull was therefore reported as HeldByRunningJob, and because the same pod also showed up stuck‑Pending in Pod health, summarizeDoctor rolled it up to "a training is already running, wait for it to finish" at exit 0. The operator was told to wait on a pod that was never going to run.

Closes tracebloc/backend#3247. This is the Bugbot (High) finding that blocked the develop→staging hop (cli#641, thread #641 (comment)).

Fix

A Pending pod (even with a NodeName) is not running:

  • checkNodeFit routes a job-name pod into the running‑job sums only when its phase is genuinely Running. An assigned‑but‑Pending one feeds neither the job nor the steady‑state sums, so it can no longer emit HeldByRunningJob.
  • Past the same grace window checkPods uses, such a pod is recorded and surfaced as an actionable Fail under a new StuckJobPod prefix that names its real state (ImagePullBackOff / ContainerCreating / Pending) and tells the operator to inspect the pod, not wait. A run legitimately waiting for a running job has no NodeName, so it is never mistaken for a wedged one.
  • summarizeDoctor gains a matching arm placed above the inferred stuck‑Pending arm (a measured cause beats an inference), so a wedged pod fails at exit 2 with the right remedy instead of the "wait for the running job" Warn at exit 0.

The genuinely‑running case is unchanged.

Tests

  • internal/doctorTestCheckNodeFitStuckJobPod pins each pod state: Running (→ HeldByRunningJob, unchanged), assigned‑Pending past grace (→ StuckJobPod Fail naming ImagePullBackOff / ContainerCreating / bare Pending), within‑grace (→ neither, node OK), a non‑Job Pending pod (scope guard), and a stuck pod co‑existing with a running job (the stuck Fail wins).
  • internal/clisummarizeDoctor pins that a scheduled‑but‑stuck training pod is a Fail (exit 2), never the "wait for a running job" Warn, both with and without Pod health also flagging it.
  • Copy‑catalog golden regenerated for the new user‑facing strings.
  • go build ./... && go vet ./... && go test ./... all green.

Version‑bump gate

No VERSION change: develop already carries the pending unreleased bump (0.10.24; the latest release tag is v0.10.23), so this PR ships published‑path changes under an untagged version — the gate's documented pass. Hand‑bumping would be a forbidden manual version bump.

— drafted with Claude Code


Note

Medium Risk
Changes doctor verdict and exit codes for a specific cluster state (scheduled training pods wedged on pull/create); logic is heavily tested but misclassification would still mislead operators on training readiness.

Overview
Fixes backend#3247: tracebloc doctor could exit 0 and tell users to wait for a “running” training job when the pod was actually scheduled but stuck (e.g. image pull backoff).

Node capacity (checkNodeFit) now counts batch job pods toward “room held by a running job” only when phase is Running. Assigned-but-Pending job pods with kubelet-reported wedged reasons (ImagePullBackOff, ErrImagePull, create errors, etc., past the same grace as Pod health) surface a new StuckJobPod Fail with inspect-the-pod guidance—not capacity or wait advice. Still-pulling/creating states stay on the existing age-based path so cold large pulls don’t false-positive.

Rollup (summarizeDoctor) adds an arm for that measured Fail (above generic stuck-Pending inference), so readiness is Not ready — stuck starting at exit 2 instead of the transient “wait for the running job” Warn.

Tests cover node-fit classification, rollup behavior, and updated user-facing string golden file.

Reviewed by Cursor Bugbot for commit 0e28595. Bugbot is set up for automated code reviews on this repo. Configure here.

…nning job (backend#3247)

checkNodeFit counted every non-terminal `job-name` pod with a NodeName into
the "a running job holds the room" sum. A training pod scheduled to a node but
still Pending (ImagePullBackOff / ContainerCreating) was therefore reported as
HeldByRunningJob, and combined with the same pod showing up stuck-Pending in
Pod health, summarizeDoctor rolled it up to "a training is already running,
wait for it to finish" at exit 0 — on a pod that is wedged and never will. The
operator waits forever on a dead pod.

A Pending pod (even with a NodeName) is not running:
- checkNodeFit routes a `job-name` pod into the running-job sums only when it
  is genuinely Running; an assigned-but-Pending one feeds neither sum, so it
  can no longer emit HeldByRunningJob.
- past the same grace window checkPods uses, such a pod is recorded and
  surfaced as an actionable Fail (new StuckJobPod prefix) that names its real
  state (ImagePullBackOff / ContainerCreating / Pending) and says to inspect
  the pod, not wait. A run legitimately waiting for a running job has no
  NodeName, so it is never mistaken for a wedged one.
- summarizeDoctor gains a matching arm above the inferred stuck-Pending arm
  (measured beats inferred), so the wedged pod fails at exit 2 with the right
  remedy instead of the "wait for the running job" Warn at exit 0.

The genuinely-running case is unchanged. Tests pin each pod state (Running vs
assigned-Pending vs ImagePullBackOff/ContainerCreating vs within-grace) at both
the checkNodeFit and summarizeDoctor levels.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@aptracebloc aptracebloc self-assigned this Sep 7, 2026
Comment thread internal/cli/doctor.go Outdated
Comment thread internal/doctor/doctor.go

@saqlainsyed007 saqlainsyed007 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Core fix is sound and well-tested — routing genuinely-Running Job pods into the capacity sums while carrying assigned-but-Pending ones out to a dedicated StuckJobPod Fail correctly stops doctor telling operators to "wait for a running job" on a wedged pod (backend#3247). Verdict-switch ordering (checkNodeFit and summarizeDoctor) is internally consistent, the NodeName/terminal guards are in place, and the tests are derived-not-restated and mutation-proof. Two points before approval: (1) doctor.go:545 is the only summarizeDoctor rollup line using kubectl, breaking the plain-terms invariant this function documents three times and its own comment restates — trim to end at "…names the pod." (2) the escalation labels still-progressing ContainerCreating/Pending as "Waiting will not clear it," which can misfire on a large first-pull on a cold node. Neither is a correctness crash; happy to approve once addressed or waved.

…lup plain-terms (backend#3247)

Addresses Saqlain's review on cli#642:

- summarizeDoctor's stuck-pod ready line no longer names `kubectl` — it ends at
  "… names the pod", restoring the plain-terms invariant the function documents
  three times. The kubectl form stays in the granular checkNodeFit remedy, one
  --verbose away.
- checkNodeFit escalates to the StuckJobPod Fail only for a genuinely-wedged
  waiting reason (ImagePullBackOff / ErrImagePull / ErrImageNeverPull /
  InvalidImageName / CreateContainerConfigError / CreateContainerError), via a
  new stuckReasonWedged allowlist. A pod still pulling/creating past the grace
  window (ContainerCreating / Pulling / bare Pending) is left to checkPods'
  age-based inference, so "waiting will not clear it" is only asserted where the
  kubelet has actually reported a failure — a large first pull on a cold node no
  longer trips the confident Fail.

Tests updated to pin wedged reasons (→ stuck Fail) apart from still-progressing
ones (→ deferred, node OK), and to guard the rolled-up line against Kubernetes
vocabulary. Copy-catalog golden regenerated. go build/vet/test ./... green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@aptracebloc

Copy link
Copy Markdown
Contributor Author

bugbot run

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 0e28595. Configure here.

@saqlainsyed007 saqlainsyed007 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both findings from my earlier review are genuinely resolved in 0e28595. Approving.

Finding 1 — kubectl in the summarizeDoctor rollup line (plain-terms invariant): Fixed. The rolled-up ready line now ends at "Waiting will not clear it — %s doctor --verbose names the pod." — the kubectl describe pod phrase is gone, so summarizeDoctor holds the no-Kubernetes-vocabulary contract it documents three times. The kubectl form correctly remains one --verbose away in the granular checkNodeFit remedy. A test now guards the rolled-up remedy against kubectl and asserts it points at --verbose, so the invariant won't silently regress.

Finding 2 — "Waiting will not clear it" misfiring on a still-progressing pull: Fixed correctly by scoping the measured Fail to genuinely-wedged reasons via the new stuckReasonWedged allowlist (ImagePullBackOff, ErrImagePull, ErrImageNeverPull, InvalidImageName, CreateContainerConfigError, CreateContainerError). A pod still ContainerCreating/Pulling/bare Pending past grace is left to checkPods' age-based inference, so a large first pull on a cold node no longer trips the "will not clear it" assertion — the escalation fires only where the kubelet actually reported a failure. TestCheckNodeFitStuckJobPod pins wedged vs still-progressing states separately, plus within-grace, non-Job scope, and stuck-outranks-running-job cases.

Node-fit logic is sound: non-Running job pods feed neither resource sum, so a wedged pod can no longer emit HeldByRunningJob; the StuckJobPod Fail sits below the measured capacity Fails and above the transient Warn. CI fully green. LGTM.

@aptracebloc
aptracebloc merged commit 66b73d0 into develop Sep 7, 2026
33 checks passed
@aptracebloc
aptracebloc deleted the fix/3247-doctor-pending-not-running branch September 7, 2026 09:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants