diff --git a/src/frontend/schedulerclient/httpclient.go b/src/frontend/schedulerclient/httpclient.go index b7b7add..f21c726 100644 --- a/src/frontend/schedulerclient/httpclient.go +++ b/src/frontend/schedulerclient/httpclient.go @@ -308,8 +308,7 @@ func (hc *HTTPClient) getJobs(req *resty.Request, url string, filter JobFilter) jobs := make([]*URMJob, 0, len(rows.Rows)) for i := range rows.Rows { j := rows.Rows[i] - rawStatus := j.rowStr("job_status") - urmStatus := JobStatusFromString(rawStatus) + urmStatus := rowURMStatus(j) // Apply URM status filter in post-processing 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"), Name: j.rowStr("job_name"), Status: urmStatus, - RawStatus: rawStatus, + RawStatus: rawStatusString(j), Queue: j.rowStr("queue_name"), Nodes: j.rowInt("num_hosts"), Cores: numProcessors, @@ -372,7 +371,7 @@ func (hc *HTTPClient) getJobs(req *resty.Request, url string, filter JobFilter) Extensions: map[string]any{ "command": j.rowStr("command"), "project_name": j.rowStr("project_name"), - "pending_reason": j.rowStr("pending_reason"), + "pending_reason": j.rowJSON("pending_reason"), "resreq": j.rowStr("resreq"), "exit_status_raw": j.rowStr("exit_status"), "num_processors": numProcessors, @@ -413,6 +412,40 @@ type apiRow struct { } // 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 { raw, ok := r.Data[key] if !ok || string(raw) == "null" { diff --git a/src/frontend/schedulerclient/statusmap.go b/src/frontend/schedulerclient/statusmap.go index 4270520..ed7c5b1 100644 --- a/src/frontend/schedulerclient/statusmap.go +++ b/src/frontend/schedulerclient/statusmap.go @@ -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 ───────────────────────────────────────────────────── // QueueStatusFromString maps the apiserver's composite queue status string diff --git a/src/frontend/schedulerclient/statusmap_test.go b/src/frontend/schedulerclient/statusmap_test.go index fb918e5..e4fb1fe 100644 --- a/src/frontend/schedulerclient/statusmap_test.go +++ b/src/frontend/schedulerclient/statusmap_test.go @@ -1,6 +1,9 @@ package schedulerclient -import "testing" +import ( + "fmt" + "testing" +) // ── 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 ──────────────────────────────────────────────── func TestQueueStatusFromString(t *testing.T) {