fix(frontend): count host job-state columns and EXEC_HOST by real slots

NJOBS/RUN/SSUSP/USUSP (xcecli hosts list -l) and EXEC_HOST (xcecli jobs
list) always showed a hardcoded 1 slot per host regardless of a job's
actual per-host allocation -- rawJobToModel()'s ExecHosts JSONB builder
never read the real per-host "slots" resource from the C job allocation,
so the already-correct downstream slot-counting logic (computeHostSlotUsage,
bug-1743) and EXEC_HOST's "N*host" formatter never had real data to work
with. convert.go now extracts each host's real slots allocation via a new
CGO indexing helper, mirroring the scheduler's own get_nslots_in_alloc()
convention.

Also wires hosts list -l's "CURRENT LOAD USED FOR SCHEDULING" table (tmp/
swap/mem/slots and friends) to the host's real live resource values --
previously entirely hardcoded placeholder strings, never connected to any
backend data.
This commit is contained in:
Rongsong Shen 2026-09-07 07:32:00 +00:00
parent f55b49f1a7
commit e395eaa3ad
No known key found for this signature in database
GPG key ID: 5BCDAAE0123E9FE2
5 changed files with 223 additions and 28 deletions

View file

@ -9,6 +9,10 @@ package backend
// Helper to index into xce_host_alloc_t** (CGO cannot dereference C pointer-to-pointer arrays).
static xce_host_alloc_t *job_alloc_host(xce_job_alloc_t *a, int i) { return a->allocs[i]; }
// Helper to index into xce_alloc_t** (same CGO limitation as above) -- one
// host's per-resource allocation list, scanned for the "slots" entry.
static xce_alloc_t *host_alloc_res(xce_host_alloc_t *h, int i) { return h->allocs[i]; }
*/
import "C"
@ -114,9 +118,24 @@ func cJobToModel(job *C.xce_job_t) *models.JobInfo {
n := int(job.allocs.nhosts)
for i := 0; i < n; i++ {
h := C.job_alloc_host(job.allocs, C.int(i))
if h != nil && h.host != nil {
r.execHosts = append(r.execHosts, C.GoString(h.host))
if h == nil || h.host == nil {
continue
}
// bug-1743-followup: this host's own "slots" resource
// allocation (xce_host_alloc_t.allocs[]), mirroring the C
// scheduler's own get_nslots_in_alloc()/repl_worker_alloc_
// nslots() resname scan -- defaults to 1 when absent, same as
// those.
nslots := 1
nallocs := int(h.nallocs)
for j := 0; j < nallocs; j++ {
res := C.host_alloc_res(h, C.int(j))
if res != nil && res.resname != nil && C.GoString(res.resname) == "slots" {
nslots = int(res.value)
break
}
}
r.execHosts = append(r.execHosts, rawHostSlot{host: C.GoString(h.host), slots: nslots})
}
}
return rawJobToModel(r)

View file

@ -130,6 +130,16 @@ type rawSubmitData struct {
fromHost string
}
// rawHostSlot mirrors one xce_host_alloc_t's host name plus its "slots"
// resource allocation count (xce_host_alloc_t.allocs[], scanned for
// resname=="slots" -- mirrors the C scheduler's own get_nslots_in_alloc()/
// repl_worker_alloc_nslots() convention, defaulting to 1 when no explicit
// slots entry is present).
type rawHostSlot struct {
host string
slots int
}
// rawJobData mirrors the C xce_job_t fields used by the Go model.
type rawJobData struct {
jobID int64
@ -140,7 +150,7 @@ type rawJobData struct {
startTime int64
finishTime int64
submit *rawSubmitData
execHosts []string
execHosts []rawHostSlot
}
// rawWorkerData mirrors the C xce_worker_t fields used by the Go model.
@ -225,7 +235,7 @@ func rawJobToModel(r rawJobData) *models.JobInfo {
}
slots := make([]hostSlot, len(r.execHosts))
for i, h := range r.execHosts {
slots[i] = hostSlot{HostName: h, NumSlots: 1}
slots[i] = hostSlot{HostName: h.host, NumSlots: h.slots}
}
data, _ := json.Marshal(execHostsJSON{HostSlots: slots, NumHosts: len(slots)})
info.ExecHosts = models.Jsonb{String: string(data)}

View file

@ -2,6 +2,7 @@ package backend
import (
"apiserver/models"
"encoding/json"
"testing"
)
@ -60,6 +61,80 @@ func TestRawJobToModel_BasicFields(t *testing.T) {
}
}
// bug-1743-followup: rawJobToModel must render each host's REAL per-host
// slot count into ExecHosts' JSONB "num_slots" field (consumed by xcecli's
// EXEC_HOST column, "N*host", and by controllers/hosts.go's
// computeHostSlotUsage() for the NJOBS/RUN/SSUSP/USUSP columns) -- not a
// hardcoded 1 regardless of how many slots the job actually consumed on
// that host.
func TestRawJobToModel_ExecHostsUsesRealPerHostSlotCount(t *testing.T) {
r := rawJobData{
jobID: 1,
state: 2, // JOB_STAT_RUN
execHosts: []rawHostSlot{
{host: "pc13v", slots: 4},
},
}
info := rawJobToModel(r)
var parsed struct {
HostSlots []struct {
HostName string `json:"host_name"`
NumSlots int `json:"num_slots"`
} `json:"host_slots"`
NumHosts int `json:"num_hosts"`
}
if err := json.Unmarshal([]byte(info.ExecHosts.String), &parsed); err != nil {
t.Fatalf("failed to unmarshal ExecHosts JSONB: %v", err)
}
if len(parsed.HostSlots) != 1 {
t.Fatalf("HostSlots: got %d entries, want 1", len(parsed.HostSlots))
}
if parsed.HostSlots[0].HostName != "pc13v" {
t.Errorf("HostName: got %q, want %q", parsed.HostSlots[0].HostName, "pc13v")
}
if parsed.HostSlots[0].NumSlots != 4 {
t.Errorf("NumSlots: got %d, want 4 (must reflect the job's real per-host allocation, not a hardcoded 1)",
parsed.HostSlots[0].NumSlots)
}
}
// A job spanning multiple hosts with DIFFERENT slot counts (e.g. an
// asymmetric multi-host allocation) must preserve each host's own count
// independently, not collapse them to a single shared value.
func TestRawJobToModel_ExecHostsPreservesPerHostSlotCountsAcrossHosts(t *testing.T) {
r := rawJobData{
jobID: 2,
state: 2,
execHosts: []rawHostSlot{
{host: "hostA", slots: 2},
{host: "hostB", slots: 6},
},
}
info := rawJobToModel(r)
var parsed struct {
HostSlots []struct {
HostName string `json:"host_name"`
NumSlots int `json:"num_slots"`
} `json:"host_slots"`
}
if err := json.Unmarshal([]byte(info.ExecHosts.String), &parsed); err != nil {
t.Fatalf("failed to unmarshal ExecHosts JSONB: %v", err)
}
if len(parsed.HostSlots) != 2 {
t.Fatalf("HostSlots: got %d entries, want 2", len(parsed.HostSlots))
}
want := map[string]int{"hostA": 2, "hostB": 6}
for _, hs := range parsed.HostSlots {
if want[hs.HostName] != hs.NumSlots {
t.Errorf("host %q: got %d slots, want %d", hs.HostName, hs.NumSlots, want[hs.HostName])
}
}
}
// bug-1676: FromHost must round-trip through rawSubmitData into JobInfo like
// every other submit-time field, and must be empty-safe (no submit data, or
// submit data with no from_host, must not panic and must leave FromHost "").