forked from lei.chen/myxce
fix(schedulerclient): parse numeric job_status + object-valued pending_reason
Two bugs surfaced by the new scheduler_pend_reasons tool: 1. The apiserver returns job_status as the numeric XCE state bitmask (PEND=0x01, RUN=0x02, EXIT=0x04, DONE=0x08), but getJobs read it with rowStr (string), which returns "" for a number → every job mapped to URM "unknown", so the pending/running status filter matched nothing. → JobStatusFromXCEState(int) + rowURMStatus fallback. 2. pending_reason is a JSON object in the row, not a string; rowStr returned "" so Extensions["pending_reason"] was empty and the reason never parsed. → rowJSON(key) stores the raw JSON value; the MCP pend_reasons parser handles the map form. schedulerclient tests updated (JobStatusFromXCEState table). mcp-server + schedulerclient test suites pass; live MCP call now returns the bjobs -p style explanation for a PEND job. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
67abbfa492
commit
53c0083410
3 changed files with 89 additions and 5 deletions
|
|
@ -308,8 +308,7 @@ func (hc *HTTPClient) getJobs(req *resty.Request, url string, filter JobFilter)
|
||||||
jobs := make([]*URMJob, 0, len(rows.Rows))
|
jobs := make([]*URMJob, 0, len(rows.Rows))
|
||||||
for i := range rows.Rows {
|
for i := range rows.Rows {
|
||||||
j := rows.Rows[i]
|
j := rows.Rows[i]
|
||||||
rawStatus := j.rowStr("job_status")
|
urmStatus := rowURMStatus(j)
|
||||||
urmStatus := JobStatusFromString(rawStatus)
|
|
||||||
|
|
||||||
// Apply URM status filter in post-processing
|
// Apply URM status filter in post-processing
|
||||||
if len(filter.Statuses) > 0 {
|
if len(filter.Statuses) > 0 {
|
||||||
|
|
@ -358,7 +357,7 @@ func (hc *HTTPClient) getJobs(req *resty.Request, url string, filter JobFilter)
|
||||||
HumanID: j.rowStr("job_id_human"),
|
HumanID: j.rowStr("job_id_human"),
|
||||||
Name: j.rowStr("job_name"),
|
Name: j.rowStr("job_name"),
|
||||||
Status: urmStatus,
|
Status: urmStatus,
|
||||||
RawStatus: rawStatus,
|
RawStatus: rawStatusString(j),
|
||||||
Queue: j.rowStr("queue_name"),
|
Queue: j.rowStr("queue_name"),
|
||||||
Nodes: j.rowInt("num_hosts"),
|
Nodes: j.rowInt("num_hosts"),
|
||||||
Cores: numProcessors,
|
Cores: numProcessors,
|
||||||
|
|
@ -372,7 +371,7 @@ func (hc *HTTPClient) getJobs(req *resty.Request, url string, filter JobFilter)
|
||||||
Extensions: map[string]any{
|
Extensions: map[string]any{
|
||||||
"command": j.rowStr("command"),
|
"command": j.rowStr("command"),
|
||||||
"project_name": j.rowStr("project_name"),
|
"project_name": j.rowStr("project_name"),
|
||||||
"pending_reason": j.rowStr("pending_reason"),
|
"pending_reason": j.rowJSON("pending_reason"),
|
||||||
"resreq": j.rowStr("resreq"),
|
"resreq": j.rowStr("resreq"),
|
||||||
"exit_status_raw": j.rowStr("exit_status"),
|
"exit_status_raw": j.rowStr("exit_status"),
|
||||||
"num_processors": numProcessors,
|
"num_processors": numProcessors,
|
||||||
|
|
@ -413,6 +412,40 @@ type apiRow struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// rowStr extracts a string field. Missing, null, or non-string → "".
|
// rowStr extracts a string field. Missing, null, or non-string → "".
|
||||||
|
// rowURMStatus reads a job's status, preferring the string form (PEND/RUN/...)
|
||||||
|
// and falling back to the numeric XCE state bitmask the apiserver returns in
|
||||||
|
// job_status (rowStr can't unmarshal a number into a string).
|
||||||
|
func rowURMStatus(r apiRow) JobStatus {
|
||||||
|
if s := r.rowStr("job_status"); s != "" {
|
||||||
|
return JobStatusFromString(s)
|
||||||
|
}
|
||||||
|
return JobStatusFromXCEState(r.rowInt("job_status"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// rawStatusString returns the backend-native status value (string form when
|
||||||
|
// present, else the numeric state), used for URMJob.RawStatus.
|
||||||
|
func rawStatusString(r apiRow) string {
|
||||||
|
if s := r.rowStr("job_status"); s != "" {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
return strconv.Itoa(r.rowInt("job_status"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// rowJSON extracts a raw JSON field as a generic value (map/string/...).
|
||||||
|
// Missing or null → nil. Used for object-valued fields like pending_reason
|
||||||
|
// that rowStr can't read (a JSON object does not unmarshal into a string).
|
||||||
|
func (r apiRow) rowJSON(key string) any {
|
||||||
|
raw, ok := r.Data[key]
|
||||||
|
if !ok || string(raw) == "null" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var v any
|
||||||
|
if err := json.Unmarshal(raw, &v); err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
func (r apiRow) rowStr(key string) string {
|
func (r apiRow) rowStr(key string) string {
|
||||||
raw, ok := r.Data[key]
|
raw, ok := r.Data[key]
|
||||||
if !ok || string(raw) == "null" {
|
if !ok || string(raw) == "null" {
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,28 @@ func JobStatusFromString(s string) JobStatus {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// JobStatusFromXCEState maps the raw numeric XCE job-state bitmask the
|
||||||
|
// apiserver returns in job_status (JOB_STAT_PEND=0x01, RUN=0x02, EXIT=0x04,
|
||||||
|
// DONE=0x08, PSUSP=0x10, USUSP=0x20, SSUSP=0x40 — src/include/xce/types.h) to
|
||||||
|
// a URM JobStatus. Composite states (e.g. RUN|DONE during cleanup) resolve to
|
||||||
|
// the most significant, non-PEND state; otherwise fall back to unknown.
|
||||||
|
func JobStatusFromXCEState(state int) JobStatus {
|
||||||
|
switch state & 0x7F {
|
||||||
|
case 0x02, 0x0A: // RUN, RUN|DONE
|
||||||
|
return JobStatusRunning
|
||||||
|
case 0x04, 0x0C: // EXIT, EXIT|DONE
|
||||||
|
return JobStatusFailed
|
||||||
|
case 0x08: // DONE
|
||||||
|
return JobStatusSucceeded
|
||||||
|
case 0x10, 0x20, 0x40: // PSUSP/USUSP/SSUSP
|
||||||
|
return JobStatusSuspended
|
||||||
|
case 0x01: // PEND
|
||||||
|
return JobStatusPending
|
||||||
|
default:
|
||||||
|
return JobStatusUnknown
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── QueueStatus mapping ─────────────────────────────────────────────────────
|
// ── QueueStatus mapping ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
// QueueStatusFromString maps the apiserver's composite queue status string
|
// QueueStatusFromString maps the apiserver's composite queue status string
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
package schedulerclient
|
package schedulerclient
|
||||||
|
|
||||||
import "testing"
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
// ── JobStatus mapping tests ─────────────────────────────────────────────────
|
// ── JobStatus mapping tests ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
@ -39,6 +42,32 @@ func TestJobStatusFromString(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestJobStatusFromXCEState(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
input int
|
||||||
|
want JobStatus
|
||||||
|
}{
|
||||||
|
{0x01, JobStatusPending}, // JOB_STAT_PEND
|
||||||
|
{0x02, JobStatusRunning}, // JOB_STAT_RUN
|
||||||
|
{0x0A, JobStatusRunning}, // RUN|DONE during cleanup
|
||||||
|
{0x04, JobStatusFailed}, // JOB_STAT_EXIT
|
||||||
|
{0x08, JobStatusSucceeded}, // JOB_STAT_DONE
|
||||||
|
{0x10, JobStatusSuspended}, // JOB_STAT_PSUSP
|
||||||
|
{0x20, JobStatusSuspended}, // JOB_STAT_USUSP
|
||||||
|
{0x40, JobStatusSuspended}, // JOB_STAT_SSUSP
|
||||||
|
{0x00, JobStatusUnknown},
|
||||||
|
{0x80, JobStatusUnknown}, // out of the 7-bit state mask
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(fmt.Sprintf("0x%02x", tt.input), func(t *testing.T) {
|
||||||
|
got := JobStatusFromXCEState(tt.input)
|
||||||
|
if got != tt.want {
|
||||||
|
t.Errorf("JobStatusFromXCEState(0x%02x) = %q, want %q", tt.input, got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── QueueStatus mapping tests ────────────────────────────────────────────────
|
// ── QueueStatus mapping tests ────────────────────────────────────────────────
|
||||||
|
|
||||||
func TestQueueStatusFromString(t *testing.T) {
|
func TestQueueStatusFromString(t *testing.T) {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue