From 6f757872c46cad9ad36d82a182bdb9aa023195bd Mon Sep 17 00:00:00 2001 From: "lei.chen" Date: Thu, 3 Sep 2026 14:13:02 +0000 Subject: [PATCH] merge-fix(apiserver/hosts): adopt theirs computeHostSlotUsage, drop our superseded job-count helper hosts.go conflict-free auto-merge left both host counter implementations: ours countRunningJobsPerHost() (bug-1490, per-RUN-job count) and theirs computeHostSlotUsage() (bug-1743, per-host occupied-SLOT count bucketed USUSP>SSUSP>RUN) wired into processHostData(), which runs after GetHostsInfo and overwrote our num_run, leaving our helper dead code. Resolution: keep theirs (slot-bucketed). Rationale: it matches the codebase's canonical JobStateStr mapping (a RUN|USUSP job is USUSP, not RUN -- our RUN-bit-only check mislabeled suspended jobs as running and left num_ssusp/num_ususp permanently 0), sums num_slots so values are comparable to max_slot, and is applied uniformly at the processHostData choke point for all host endpoints. Deleted countRunningJobsPerHost + GetHostsInfo fill-loop and rewrote hosts_numrun_test.go to cover computeHostSlotUsage's bucket/multi-slot semantics plus our retained robustness cases (PEND/DONE and empty/bad exec_hosts ignored). Theirs' host_control_test.go TestProcessHostData_ComputesSlotUsageFromJobs provides the processHostData integration coverage. Co-Authored-By: Claude Code --- src/frontend/apiserver/controllers/hosts.go | 39 -------- .../controllers/hosts_numrun_test.go | 95 +++++++++++-------- 2 files changed, 55 insertions(+), 79 deletions(-) diff --git a/src/frontend/apiserver/controllers/hosts.go b/src/frontend/apiserver/controllers/hosts.go index ee298b5f..2b59c4cf 100644 --- a/src/frontend/apiserver/controllers/hosts.go +++ b/src/frontend/apiserver/controllers/hosts.go @@ -190,20 +190,6 @@ func (h *HostHandler) GetHostsInfo(ctx *gin.Context) { 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) @@ -211,31 +197,6 @@ 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 index 578d9ba3..60319993 100644 --- a/src/frontend/apiserver/controllers/hosts_numrun_test.go +++ b/src/frontend/apiserver/controllers/hosts_numrun_test.go @@ -7,53 +7,68 @@ import ( "apiserver/store" ) -func TestCountRunningJobsPerHost(t *testing.T) { +// TestComputeHostSlotUsage_BucketsSlotsByStateAndRobustness covers the +// merged host-slot derivation (bug-1743 computeHostSlotUsage, which replaced +// our bug-1490-era countRunningJobsPerHost on merge): per-host occupied-slot +// totals bucketed USUSP > SSUSP > RUN, matching models.JobStateStr's own +// canonical mapping (a RUN|USUSP job is USUSP, not RUN). It keeps the +// robustness cases our old job-count helper tested: multi-host jobs count on +// every host they occupy, multi-slot jobs sum num_slots, and PEND/DONE or +// empty/bad exec_hosts are ignored without crashing. +func TestComputeHostSlotUsage_BucketsSlotsByStateAndRobustness(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}`}, - }) + putJob := func(id uint64, execHosts string, status int) { + store.Jobs.Put(id, models.JobInfo{ + JobStatus: status, + ExecHosts: models.Jsonb{String: execHosts}, + }) + } + + // Plain RUN job on t1 (1 slot). + putJob(1, `{"host_slots":[{"host_name":"t1","num_slots":1}],"num_hosts":1}`, models.JOBSTAT["RUN"]) + // RUN|USUSP job on t2 — must count as USUSP, not RUN (JobStateStr order). + putJob(2, `{"host_slots":[{"host_name":"t2","num_slots":1}],"num_hosts":1}`, models.JOBSTAT["RUN"]|models.JOBSTAT["USUSP"]) + // Multi-host RUN job: one job on t1 + t3 counts on both hosts. + putJob(3, `{"host_slots":[{"host_name":"t1","num_slots":1},{"host_name":"t3","num_slots":1}],"num_hosts":2}`, models.JOBSTAT["RUN"]) + // Multi-slot RUN job: 3 slots on t4 must sum to 3, not count as 1. + putJob(4, `{"host_slots":[{"host_name":"t4","num_slots":3}],"num_hosts":1}`, models.JOBSTAT["RUN"]) + // RUN|SSUSP job on t5 using 2 slots — counts as SSUSP. + putJob(5, `{"host_slots":[{"host_name":"t5","num_slots":2}],"num_hosts":1}`, models.JOBSTAT["RUN"]|models.JOBSTAT["SSUSP"]) // 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`}, - }) + putJob(6, `{"host_slots":[{"host_name":"t3","num_slots":1}],"num_hosts":1}`, models.JOBSTAT["PEND"]) + putJob(7, `{"host_slots":[{"host_name":"t1","num_slots":1}],"num_hosts":1}`, models.JOBSTAT["DONE"]) + // RUN job with empty / bad exec_hosts must not crash or count. + putJob(8, ``, models.JOBSTAT["RUN"]) + putJob(9, `not-json`, models.JOBSTAT["RUN"]) - counts := countRunningJobsPerHost() + counts := computeHostSlotUsage() - if counts["t1"] != 2 { // jobs 1 + 3 - t.Errorf("t1 running = %d, want 2", counts["t1"]) + // t1: job1 (RUN, 1) + job3 (RUN, 1) = 2 RUN slots. + if got := counts["t1"].Run; got != 2 { + t.Errorf("t1 Run = %d, want 2", got) } - if counts["t2"] != 1 { - t.Errorf("t2 running = %d, want 1", counts["t2"]) + if got := counts["t1"].SSusp + counts["t1"].USusp; got != 0 { + t.Errorf("t1 susp buckets = %d, want 0", got) } - if counts["t3"] != 1 { // job 3 only (job 4 is PEND) - t.Errorf("t3 running = %d, want 1", counts["t3"]) + // t2: RUN|USUSP job must land in USusp, not Run. + if got := counts["t2"].USusp; got != 1 { + t.Errorf("t2 USusp = %d, want 1", got) + } + if got := counts["t2"].Run; got != 0 { + t.Errorf("t2 Run = %d, want 0 (RUN|USUSP is USUSP)", got) + } + // t3: job3 only (job 6 is PEND). + if got := counts["t3"].Run; got != 1 { + t.Errorf("t3 Run = %d, want 1", got) + } + // t4: 3-slot RUN job sums to 3. + if got := counts["t4"].Run; got != 3 { + t.Errorf("t4 Run = %d, want 3 (multi-slot sums)", got) + } + // t5: RUN|SSUSP 2-slot job lands in SSusp. + if got := counts["t5"].SSusp; got != 2 { + t.Errorf("t5 SSusp = %d, want 2", got) } if _, ok := counts["nohost"]; ok { t.Error("unexpected host 'nohost' in counts")