fix(apiserver): populate hosts-list slot usage counts
xcecli hosts list showed NUM_RUN/NUM_SSUSP/NUM_USUSP as always 0 -- the fields existed on HostInfo and were displayed, but nothing ever assigned them. processHostData now derives per-host slot usage from the live job store (exec_hosts + job_status), counting occupied slots so the numbers are comparable to MAX_SLOT. Partial fix for gitea#46 / bug-1743: the separate "dispatch stops at 8/16" question needs the reporter's live limits.yaml/pend-reason to confirm a usergroup/queue slot limit, not addressed here. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y4M5MJM2zK6nU43da267Ku
This commit is contained in:
parent
343639fc7e
commit
728496bea0
3 changed files with 155 additions and 0 deletions
|
|
@ -137,3 +137,63 @@ func TestPreCheckHostControlRequest_CommentTooLong(t *testing.T) {
|
|||
t.Error("expected error for comment exceeding 512 chars, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
// TestProcessHostData_ComputesSlotUsageFromJobs is a regression guard for
|
||||
// bug-1743 (gitea #46): models.HostInfo.NumRun/.NumSSusp/.NumUSusp exist in
|
||||
// the schema and are already shown by `xcecli hosts list`, but nothing in
|
||||
// the apiserver ever assigned them -- they were permanently stuck at Go's
|
||||
// zero value, so `hosts list` always showed NUM_RUN=0 regardless of how many
|
||||
// jobs were actually running on a host. processHostData must now derive
|
||||
// these counts from store.Jobs' live exec_hosts/job_status data (the same
|
||||
// JSONB shape backend/convert_pure.go's job-conversion path populates:
|
||||
// {"host_slots":[{"host_name":..,"num_slots":N}],"num_hosts":N}), counting
|
||||
// slots (not just job count) so the numbers are comparable to max_slot.
|
||||
func TestProcessHostData_ComputesSlotUsageFromJobs(t *testing.T) {
|
||||
resetHostControlState()
|
||||
|
||||
store.Hosts.Put("test01", models.HostInfo{
|
||||
HostName: "test01",
|
||||
NumCPUs: 16,
|
||||
MaxSlot: 16,
|
||||
})
|
||||
|
||||
putJob := func(id int64, execHosts string, status int) {
|
||||
store.Jobs.Put(uint64(id), models.JobInfo{
|
||||
Unique_JobID: id,
|
||||
JobID: id,
|
||||
JobStatus: status,
|
||||
ExecHosts: models.Jsonb{String: execHosts},
|
||||
})
|
||||
}
|
||||
|
||||
// 6 plain RUN jobs, 1 slot each on test01.
|
||||
for i := int64(1); i <= 6; i++ {
|
||||
putJob(i, `{"host_slots":[{"host_name":"test01","num_slots":1}],"num_hosts":1}`, models.JOBSTAT["RUN"])
|
||||
}
|
||||
// 1 user-suspended job (RUN|USUSP) -- must count as USUSP, not RUN.
|
||||
putJob(7, `{"host_slots":[{"host_name":"test01","num_slots":1}],"num_hosts":1}`, models.JOBSTAT["RUN"]|models.JOBSTAT["USUSP"])
|
||||
// 1 system-suspended job (RUN|SSUSP) using 2 slots on test01.
|
||||
putJob(8, `{"host_slots":[{"host_name":"test01","num_slots":2}],"num_hosts":1}`, models.JOBSTAT["RUN"]|models.JOBSTAT["SSUSP"])
|
||||
// 1 PEND job with no exec host yet -- must not be counted anywhere.
|
||||
putJob(9, "", models.JOBSTAT["PEND"])
|
||||
// 1 job on a different host -- must not count toward test01.
|
||||
putJob(10, `{"host_slots":[{"host_name":"other-host","num_slots":1}],"num_hosts":1}`, models.JOBSTAT["RUN"])
|
||||
|
||||
rows := store.StoreToOrderMaps(store.Hosts)
|
||||
result := processHostData(rows, "")
|
||||
|
||||
if result.Count != 1 {
|
||||
t.Fatalf("expected 1 host row, got %d", result.Count)
|
||||
}
|
||||
row := result.Rows[0]
|
||||
|
||||
if got := row.Data["num_run"]; got != 6 {
|
||||
t.Errorf("num_run = %v, want 6", got)
|
||||
}
|
||||
if got := row.Data["num_ssusp"]; got != 2 {
|
||||
t.Errorf("num_ssusp = %v, want 2 (2-slot SSUSP job)", got)
|
||||
}
|
||||
if got := row.Data["num_ususp"]; got != 1 {
|
||||
t.Errorf("num_ususp = %v, want 1", got)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import (
|
|||
"apiserver/store"
|
||||
"apiserver/utils"
|
||||
logger "apiserver/utils"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
|
@ -565,7 +566,56 @@ func preCheckHostControlRequest(req *models.HostControlRequest) (bool, error) {
|
|||
return isAll, nil
|
||||
}
|
||||
|
||||
// hostSlotUsage holds counts of slots currently occupied on one host,
|
||||
// broken down by run/suspend sub-state, mirroring HostInfo.NumRun/
|
||||
// NumSSusp/NumUSusp.
|
||||
type hostSlotUsage struct {
|
||||
Run, SSusp, USusp int
|
||||
}
|
||||
|
||||
// execHostsJSON mirrors the JSONB shape backend/convert_pure.go's
|
||||
// buildJobInfoFromReplayJob() writes into JobInfo.ExecHosts.
|
||||
type execHostsJSON struct {
|
||||
HostSlots []struct {
|
||||
HostName string `json:"host_name"`
|
||||
NumSlots int `json:"num_slots"`
|
||||
} `json:"host_slots"`
|
||||
}
|
||||
|
||||
// computeHostSlotUsage derives per-host slot usage from the live job store,
|
||||
// since store.Hosts' own NumRun/NumSSusp/NumUSusp fields are never written
|
||||
// (bug-1743): a host's occupied-slot count can only be known by scanning
|
||||
// which non-terminal jobs are currently placed on it.
|
||||
func computeHostSlotUsage() map[string]hostSlotUsage {
|
||||
usage := make(map[string]hostSlotUsage)
|
||||
for _, job := range store.Jobs.List() {
|
||||
if job.ExecHosts.String == "" {
|
||||
continue
|
||||
}
|
||||
var parsed execHostsJSON
|
||||
if err := json.Unmarshal([]byte(job.ExecHosts.String), &parsed); err != nil {
|
||||
continue
|
||||
}
|
||||
for _, hs := range parsed.HostSlots {
|
||||
u := usage[hs.HostName]
|
||||
switch {
|
||||
case job.JobStatus&models.JOBSTAT["USUSP"] != 0:
|
||||
u.USusp += hs.NumSlots
|
||||
case job.JobStatus&models.JOBSTAT["SSUSP"] != 0:
|
||||
u.SSusp += hs.NumSlots
|
||||
case job.JobStatus&models.JOBSTAT["RUN"] != 0:
|
||||
u.Run += hs.NumSlots
|
||||
default:
|
||||
continue
|
||||
}
|
||||
usage[hs.HostName] = u
|
||||
}
|
||||
}
|
||||
return usage
|
||||
}
|
||||
|
||||
func processHostData(rows []utils.OrderMap, page_token string) utils.QueryResult {
|
||||
slotUsage := computeHostSlotUsage()
|
||||
for _, row := range rows {
|
||||
if v, exist := row.Data["host_status"]; exist {
|
||||
if v != nil {
|
||||
|
|
@ -574,6 +624,16 @@ func processHostData(rows []utils.OrderMap, page_token string) utils.QueryResult
|
|||
}
|
||||
}
|
||||
|
||||
// bug-1743: HostInfo.NumRun/.NumSSusp/.NumUSusp are never assigned
|
||||
// anywhere else -- derive them here from the live job store so
|
||||
// `hosts list` reflects actual slot usage instead of always 0.
|
||||
if hostName, ok := row.Data["host_name"].(string); ok {
|
||||
usage := slotUsage[hostName]
|
||||
row.Data["num_run"] = usage.Run
|
||||
row.Data["num_ssusp"] = usage.SSusp
|
||||
row.Data["num_ususp"] = usage.USusp
|
||||
}
|
||||
|
||||
// Process JSONB fields based on predefined list
|
||||
for _, jsonbField := range models.JSONBHFields {
|
||||
if v, exist := row.Data[jsonbField]; exist {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue