Skip to content

Classification

Find, Inspect and Watch report each object as a model.ObjectObs: a state, whether an administrator holds it, a cause with a kind, a provenance and a detail, and the execution it is on. This page lists every rule the adapter applies, in the order it applies them, and the exact text it puts in the detail. go/dispatch's classification explanation says what the states and causes mean to the supervisor.

What one classification reads

Each object is classified from one read of:

  • the TrainJob;
  • its JobSet, the one with the TrainJob's name, if there is one;
  • in Kueue mode, its Workload, the one labelled kueue.x-k8s.io/job-uid with the TrainJob's UID, if there is one;
  • its pods, from one list of the namespace's pods, Terminating ones included, selected by dispatch's job-id and submission labels and separately by JobSet's jobset-name label;
  • the last state of pods that have been deleted, which only an open Watch captures;
  • the Nodes those pods are bound to, when the adapter may get Nodes.

In the tables below, words means a condition's reason and message as <reason>: <message>, or the reason alone when the message is empty. Kinds and states are written as dispatch prints them: node-lost, non-zero-exit, gated and so on.

The state, and the cause it comes with

The first rule that matches decides. Rules 1 to 5 read the TrainJob and, in Kueue mode, the Workload; rule 6 covers everything else.

1. Complete

Signal State Cause Provenance Detail
TrainJob condition Complete=True ended none none empty

2. Failed

The TrainJob condition Failed=True. The state is ended, and the cause depends on the condition's reason:

Reason Cause Provenance Detail
DeadlineExceeded time-limit scheduler the condition's words
TrainingRuntimeNotSupported launch-failed scheduler the condition's words
FailedJobs, ReachedMaxRestarts or FailJobSetFailurePolicyAction from the pods of the current execution, as in pod findings
the same, with no pod finding unknown scheduler no pod evidence was captured; JobSet: <words>, where the words are those of the JobSet's Failed condition, or the TrainJob's when the JobSet is gone
any other reason unknown scheduler TrainJob Failed: <words>

3. Gated

Mode Signal State Admin-held Cause Detail
Kueue the TrainJob's kueue.x-k8s.io/admission-gated-by lists dispatch.phpboyscout.uk/gate gated no none empty
Kueue another gate is listed on the TrainJob's annotation or the Workload's gated yes none gated by <gate>, <gate>
plain spec.suspend: true, no dispatch.phpboyscout.uk/opened: "true", and no hold-instance annotation gated no none empty

dispatch's own gate is read from the TrainJob alone. Kueue copies a gate's removal to the Workload only while the Workload is active, so a Workload held before Open can still carry dispatch's gate. The two gates explains it.

4. Held by dispatch

A dispatch hold is in force when the held object carries dispatch.phpboyscout.uk/hold-instance as a decimal of 1 or more, with no sign or leading zero. The held object is the Workload in Kueue mode and the TrainJob in plain mode.

Mode Signal State Cause
Kueue a hold in force and the Workload's spec.active: false held held, provenance actor, when the Workload has Evicted=True with a reason beginning Deactivated, dated by it; otherwise none
plain a hold in force and spec.suspend: true held held, provenance actor, when a pod of the execution was seen, dated by the TrainJob's Suspended=True transition; otherwise none

The detail of a held object is empty apart from the situation notes. The hold's instance is reported as HoldInstance whatever the state.

5. Suspended by someone else

Mode Signal State Admin-held Cause Provenance Detail
plain spec.suspend: true and opened: "true", with no dispatch hold pending yes unknown none suspended outside dispatch
Kueue the Workload is inactive with no dispatch hold, and its Evicted reason is DeactivatedDueToMaximumExecutionTimeExceeded ended no time-limit scheduler the condition's words
Kueue the same, with any other reason beginning DeactivatedDueTo ended no unknown scheduler the condition's words, then ; evictions: <reason>×<count>, ... from the Workload's scheduling stats when it has any
Kueue the same, with any other reason or none pending yes unknown none suspended outside dispatch, then ; <words> of the Evicted condition when there is one

6. Pending or running

Everything else is running when it is admitted and not suspended, and pending otherwise. Admitted means, in plain mode, always; in Kueue mode, the Workload has Admitted=True. Not suspended means spec.suspend is not true and the TrainJob has no Suspended=True condition.

The cause is the first of these that applies:

Signal When Cause Provenance Detail
the JobSet's status.restarts is above the highest J any pod shows either state requeued scheduler JobSet restarts <n>, last seen <j>, then ; ended by <kind>: <detail> from the pod findings of the execution before the current one, when it has any
among the current execution's pods that have a DisruptionTarget condition, the earliest finding is node-lost running node-lost scheduler that finding's detail
a Node the current execution runs on has Ready=False or Ready=Unknown running, Nodes readable node-lost scheduler node <name> Ready=<status>: <words>
the Workload has Evicted=True with reason Preempted pending, Kueue preempted scheduler the condition's words
the same, reason NodeFailures pending, Kueue node-lost scheduler the condition's words
the same, reason PodsReadyTimeout pending, Kueue launch-failed scheduler the condition's words
the same, any other reason pending, Kueue unknown scheduler Workload evicted: <words>
none of these either state none none empty

Without permission to read Nodes, a running object is never judged node-lost from a Node's Ready condition.

A condition the adapter does not know

When the rules leave the cause at none and the TrainJob has a condition that is True and is not Suspended, Complete or Failed, the cause becomes unknown, with no provenance, and the detail TrainJob <type>: <words>, one per such condition, separated by ;. The state is unchanged.

Pod findings

A pod reports up to two findings: one from its DisruptionTarget condition and one from its node container's termination. A pod is read the same way whether it is live or a deleted pod's captured last state.

DisruptionTarget=True

Reason The pod's Node Cause Provenance Detail
DeletionByTaintManager not readable node-lost scheduler <words>; the node's taints were not checked: Nodes are not readable
DeletionByTaintManager gone node-lost scheduler <words>; node <name> is gone
DeletionByTaintManager tainted node.kubernetes.io/unreachable, not-ready or out-of-service, or a key in nodeLossTaints node-lost scheduler <words>; node <name> tainted <key>
DeletionByTaintManager any other taints unknown none DisruptionTarget <words>; node <name> taints: <key>:<effect>, ...
PreemptionByScheduler preempted scheduler the condition's words
DeletionByPodGC gone, or tainted out-of-service node-lost scheduler <words>; node <name> <state>, where the state is gone or Ready=<status>, out-of-service
DeletionByPodGC otherwise, or not readable unknown none DisruptionTarget <words>
any other reason unknown none DisruptionTarget <words>

A finding from the condition is dated by the condition's transition.

The node container terminated

Termination Cause Provenance Exit code Detail
reason OOMKilled out-of-memory application the container's OOMKilled
any other non-zero exit code non-zero-exit application the container's exit <code>, then ; <reason> and ; <message> when set
exit code 0 no finding

A finding from the container is dated by its finishedAt.

Which finding wins

The earliest finding among the pods is the cause. On a tie, node-lost beats preempted, which beats out-of-memory, which beats non-zero-exit, which beats anything else. The detail is pod <name>: <the finding's detail>, followed by ; other ranks: <pod> <kind> (<detail>), ... for every other finding, since a rank that exits because its peer died is a consequence, not the cause.

The execution

An object's current execution is the set of its pods, live or captured, with the highest R = A + J, preferring the higher A on a tie. A comes from the pod annotation dispatch.phpboyscout.uk/admission and J from the label jobset.sigs.k8s.io/restart-attempt; a pod lacking either is not counted towards one. Execution identity explains the two counters.

Field Value
Restart R
Token <JobID>.<N>.<attempt>.<A>.<J>, the same as the execution's DISPATCH_ATTEMPT
Nodes the Nodes its pods are bound to, sorted
Started the TrainJob's last Suspended=False transition; once it is suspended again, the earliest start of a node container among the execution's pods
Ended when the state is not running and a pod of the execution was seen: the time of the event the rule above dated

Foreign

An object is reported Foreign when anything below is true. Each adds its note to the detail.

Note When
name "<name>" is not dj-<jobid>-s<N> the TrainJob's name is not dispatch's
no comment annotation dispatch.phpboyscout.uk/comment is missing
comment "<value>" does not parse the comment is not exactly dispatch:s=<N>;a=<attempt> in plain decimals
comment says submission <n>, the name <m> the comment and the name disagree
submission label "<value>" disagrees with the name or comment the submission label is present and disagrees
job-id label "<value>" disagrees with the name the job-id label is present and disagrees
JobSet UID <uid> is not the recorded <uid>: J may have been reset the JobSet is not the one recorded in dispatch.phpboyscout.uk/jobset-uid
pod <name> carries no admission annotation a live pod has no readable A
pod <name> has admission <a>, below the floor <f>: A may have been reset a live pod's A is below dispatch.phpboyscout.uk/admission-floor
runtime snapshot <name> was edited outside dispatch (generation <g>) the runtime copy's metadata.generation is above 1

A label that has been removed contradicts nothing, so stripping labels alone does not make an object foreign.

Situation

Last, the detail says what status needs to explain a stall:

State Note
running pod <name> waiting: <reason>: <message> for each live pod whose node container is waiting with a reason, such as ImagePullBackOff
any other remaining pods: <pod> on <node> (<node state>), ... for the live pods, where the node state is Ready=True, Ready=False, Ready=Unknown, gone or not readable, with , out-of-service when the Node has that taint; an unscheduled pod is <pod> on no node
pending, Kueue mode, with kueue.admissionCheck set admission check <name> <state>, then ; <message> when the check has one

The parts of the detail are joined with ;, in the order of this page: the cause's own detail, the foreign notes, then the situation.

Orphans

Find also reports what is left of a TrainJob that is gone: a JobSet, a pod or a runtime copy of the job with no live TrainJob of its UID. Each is its own object, always Foreign, with an object reference naming its kind, a cause of unknown with no provenance, and the submission from its name or its submission label.

Orphan State Detail
JobSet ended once it has a terminal state, otherwise running JobSet with no live TrainJob of its UID
pod ended when Succeeded or Failed, pending when Pending, otherwise running pod with no live TrainJob of its UID on <node>; its Node is the execution's, so Ceased can answer for it
runtime copy ended runtime snapshot with no live TrainJob of its UID: <kind>/<name>@<resourceVersion> of the runtime it was copied from

Inspect reports an orphan the same way for as long as it lasts, even if a TrainJob of its name appears again. Background deletion says why orphans are routine.

Watch

The adapter implements go/dispatch's Watcher, so it declares no PolledLoss.

Watch(ctx, job) opens watches on the namespace's TrainJobs and JobSets, on the pods labelled with the job's job-id, and in Kueue mode on the namespace's Workloads. It then runs a Find and sends the observation of every object of the job. After that, each change to the job's TrainJobs (by name prefix or job-id label), JobSets (by name prefix), pods, or the Workload of an object already sent, makes it run Find again and send each observation that differs from the last one sent for that object, ignoring the time it was seen.

The stream closes, without an error, when any of the watches expires or disconnects, when a Find fails, or when ctx ends. The caller subscribes again. go/dispatch's supervisor does so on its next pass, and treats what the stream carries as a prompt: it records only what its own Find shows.

Watch is how the adapter keeps a deleted pod's evidence. When a watched pod is deleted, the adapter keeps its last state, exit code and DisruptionTarget included, against its TrainJob, up to the latest 256 pods per TrainJob. Every later classification by that Backend reads those pods as well as the live ones, which is how a TrainJob whose pods are already gone can still be classified from them. The evidence:

  • is kept only while a Watch is open on that Backend. A pod deleted while no stream is open, including in the gap between a stream closing and the next subscription, leaves none;
  • lives in that Backend's memory. Another process, or this one after a restart, does not have it;
  • is only for pods that carry the job's job-id label.

Without it, a TrainJob that failed with FailedJobs after its pods went is classified unknown, with no pod evidence was captured in the detail.