feat(apiserver): expose PEND reason end-to-end (scheduler → API)

The pending_reason field was declared on models.JobInfo but never populated:
scheduler computed PEND reasons and the controller stored them on
job->pend_reasons, yet the data stopped there. Two gaps closed:

1. controller/apiserver.c: apiserver_job_hook_fun now handles
   HOOK_DETAIL_SETPREASON by pushing the updated job to API servers (reusing
   apiserver_job_update_fun). Without this, a job whose PEND reasons change
   never reached the API server — only JOB_NEW/UPDATE/START/EXIT events did,
   and setpreason fired none of those.

2. backend/convert.go + convert_pure.go: cJobToModel / rawJobToModel now
   extract xce_job_t.pend_reasons (main_reason + host/res reasons) and
   marshal them into models.JobInfo.PendingReason as JSONB. models/jobs.go:
   pending_reason added to JOB_DEFAULT_COLUMN so default job queries return
   it (it was only in JSONBJFields, never selected).

Verified live on t1/t2/t3: a job with resreq "type == nosuchhost" stays
PEND and returns
  "pending_reason":{"main_reason":0,"host_reasons":[{"host":"t1","reason":3},
   {"host":"t2","reason":3},{"host":"t3","reason":3}]}
(main_reason=3 PREASON_MATCH_FAIL per host) via both /jobs list, /jobs/:id
and the SSE stream. 4 new unit tests (rawJobToModel pendReason marshal +
name mapping), all apiserver backend tests pass.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
lei.chen 2026-08-13 09:16:13 +00:00
parent 7b7294dd1e
commit ac6131dbd7
6 changed files with 225 additions and 0 deletions

View file

@ -159,6 +159,47 @@ type rawSubmitData struct {
startInfo *rawStartInfo
}
// rawHostReason mirrors the C xce_host_reason_t (one host-level PEND reason).
type rawHostReason struct {
Host string `json:"host"`
Reason int `json:"reason"`
}
// rawResReason mirrors the C xce_res_reason_t (one resource-level PEND reason).
type rawResReason struct {
Resource string `json:"resource"`
Host string `json:"host"`
Reason int `json:"reason"`
}
// rawPendReason mirrors the C xce_job_preason_t attached to a PEND job by the
// controller (scheduler → setpreason → job->pend_reasons). JSON-tagged so the
// marshaled value lands in models.JobInfo.PendingReason as a JSONB object —
// processJobData's JSONBJFields pass then surfaces it in the API response.
type rawPendReason struct {
MainReason int `json:"main_reason"`
MainReasonName string `json:"main_reason_name"`
HostReasons []rawHostReason `json:"host_reasons,omitempty"`
ResReasons []rawResReason `json:"res_reasons,omitempty"`
}
// pendReasonName maps a PREASON_* main_reason code to a readable name. The
// codes are the PEND reason bitfield of xce_job_preason_t (PREASON_BAD_RESREQ/
// NO_RESOURCE/MATCH_FAIL, src/include/preason.h). Unknown codes are reported
// verbatim so a new scheduler reason never silently disappears.
func pendReasonName(code int) string {
switch code {
case 1: // PREASON_BAD_RESREQ
return "bad_resreq"
case 2: // PREASON_NO_RESOURCE
return "no_resource"
case 3: // PREASON_MATCH_FAIL
return "match_fail"
default:
return fmt.Sprintf("unknown(%d)", code)
}
}
// rawJobData mirrors the C xce_job_t fields used by the Go model.
type rawJobData struct {
jobID int64
@ -170,6 +211,7 @@ type rawJobData struct {
finishTime int64
submit *rawSubmitData
execHosts []string
pendReason *rawPendReason
}
// rawWorkerData mirrors the C xce_worker_t fields used by the Go model.
@ -258,6 +300,11 @@ func rawJobToModel(r rawJobData) *models.JobInfo {
data, _ := json.Marshal(execHostsJSON{HostSlots: slots, NumHosts: len(slots)})
info.ExecHosts = models.Jsonb{String: string(data)}
}
if r.pendReason != nil {
if data, err := json.Marshal(r.pendReason); err == nil {
info.PendingReason = models.Jsonb{String: string(data)}
}
}
return info
}