diff --git a/src/frontend/apiserver/controllers/hosts.go b/src/frontend/apiserver/controllers/hosts.go index 92c4e9de..a0b2b9d3 100644 --- a/src/frontend/apiserver/controllers/hosts.go +++ b/src/frontend/apiserver/controllers/hosts.go @@ -8,6 +8,7 @@ import ( "apiserver/store" "apiserver/utils" logger "apiserver/utils" + "encoding/json" "errors" "reflect" "strings" @@ -188,6 +189,21 @@ func (h *HostHandler) GetHostsInfo(ctx *gin.Context) { ReturnError(ctx, common.ERROR_CODE, common.ERROR_QUERY_HOSTINFO_MSG+": "+err.Error(), nil) return } + + // num_run is not populated by the controller's worker push, so compute it + // here from the apiserver's own job store — the same source the jobs list + // uses — keeping the hosts list consistent with the jobs list. + runningPerHost := countRunningJobsPerHost() + for i := range hqueryReply { + host, ok := hqueryReply[i].Data["host_name"].(string) + if !ok { + continue + } + if n, ok2 := runningPerHost[host]; ok2 { + hqueryReply[i].Data["num_run"] = n + } + } + logger.Debug("the host info query reply:", zap.Any("hqueryReply", hqueryReply), zap.String("pagetoken", pagetoken)) //convert to readable string. queryResult := processHostData(hqueryReply, pagetoken) @@ -195,6 +211,31 @@ func (h *HostHandler) GetHostsInfo(ctx *gin.Context) { ReturnSuccess(ctx, common.SUCCESS_CODE, common.SUCCESS_MSG, queryResult) } +// countRunningJobsPerHost returns the number of RUN jobs per exec host, +// derived live from the in-memory job store (store.Jobs). RUN is the bitmask +// JOBSTAT["RUN"] (0x02); exec hosts are read from each job's exec_hosts JSONB. +func countRunningJobsPerHost() map[string]int { + counts := make(map[string]int) + store.Jobs.Range(func(_ uint64, job models.JobInfo) bool { + if job.JobStatus&models.JOBSTAT["RUN"] == 0 { + return true + } + var eh struct { + HostSlots []struct { + HostName string `json:"host_name"` + } `json:"host_slots"` + } + if err := json.Unmarshal([]byte(job.ExecHosts.String), &eh); err != nil { + return true + } + for _, hs := range eh.HostSlots { + counts[hs.HostName]++ + } + return true + }) + return counts +} + func selectFields(host interface{}, fields []string) map[string]interface{} { result := make(map[string]interface{}) v := reflect.ValueOf(host) diff --git a/src/frontend/apiserver/controllers/hosts_numrun_test.go b/src/frontend/apiserver/controllers/hosts_numrun_test.go new file mode 100644 index 00000000..578d9ba3 --- /dev/null +++ b/src/frontend/apiserver/controllers/hosts_numrun_test.go @@ -0,0 +1,61 @@ +package controllers + +import ( + "testing" + + "apiserver/models" + "apiserver/store" +) + +func TestCountRunningJobsPerHost(t *testing.T) { + store.InitStores() + + // RUN job on t1. + store.Jobs.Put(1, models.JobInfo{ + JobStatus: models.JOBSTAT["RUN"], + ExecHosts: models.Jsonb{String: `{"host_slots":[{"host_name":"t1","num_slots":1}],"num_hosts":1}`}, + }) + // RUN|USUSP job on t2 — the RUN bit must still count it. + store.Jobs.Put(2, models.JobInfo{ + JobStatus: models.JOBSTAT["RUN"] | models.JOBSTAT["USUSP"], + ExecHosts: models.Jsonb{String: `{"host_slots":[{"host_name":"t2","num_slots":1}],"num_hosts":1}`}, + }) + // Multi-host RUN job: one job on t1 + t3 counts on both. + store.Jobs.Put(3, models.JobInfo{ + JobStatus: models.JOBSTAT["RUN"], + ExecHosts: models.Jsonb{String: `{"host_slots":[{"host_name":"t1","num_slots":1},{"host_name":"t3","num_slots":1}],"num_hosts":2}`}, + }) + // PEND and DONE jobs must NOT count. + store.Jobs.Put(4, models.JobInfo{ + JobStatus: models.JOBSTAT["PEND"], + ExecHosts: models.Jsonb{String: `{"host_slots":[{"host_name":"t3","num_slots":1}],"num_hosts":1}`}, + }) + store.Jobs.Put(5, models.JobInfo{ + JobStatus: models.JOBSTAT["DONE"], + ExecHosts: models.Jsonb{String: `{"host_slots":[{"host_name":"t1","num_slots":1}],"num_hosts":1}`}, + }) + // RUN job with empty/bad exec_hosts must not crash or count. + store.Jobs.Put(6, models.JobInfo{ + JobStatus: models.JOBSTAT["RUN"], + ExecHosts: models.Jsonb{String: ``}, + }) + store.Jobs.Put(7, models.JobInfo{ + JobStatus: models.JOBSTAT["RUN"], + ExecHosts: models.Jsonb{String: `not-json`}, + }) + + counts := countRunningJobsPerHost() + + if counts["t1"] != 2 { // jobs 1 + 3 + t.Errorf("t1 running = %d, want 2", counts["t1"]) + } + if counts["t2"] != 1 { + t.Errorf("t2 running = %d, want 1", counts["t2"]) + } + if counts["t3"] != 1 { // job 3 only (job 4 is PEND) + t.Errorf("t3 running = %d, want 1", counts["t3"]) + } + if _, ok := counts["nohost"]; ok { + t.Error("unexpected host 'nohost' in counts") + } +}