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:
Rongsong Shen 2026-09-01 06:37:37 +00:00
parent 343639fc7e
commit 728496bea0
No known key found for this signature in database
GPG key ID: 5BCDAAE0123E9FE2
3 changed files with 155 additions and 0 deletions

View file

@ -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 {