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 <noreply@anthropic.com>
This commit is contained in:
lei.chen 2026-09-03 14:13:02 +00:00
parent 5e2985542f
commit 6f757872c4
2 changed files with 55 additions and 79 deletions

View file

@ -190,20 +190,6 @@ func (h *HostHandler) GetHostsInfo(ctx *gin.Context) {
return 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)) logger.Debug("the host info query reply:", zap.Any("hqueryReply", hqueryReply), zap.String("pagetoken", pagetoken))
//convert to readable string. //convert to readable string.
queryResult := processHostData(hqueryReply, pagetoken) queryResult := processHostData(hqueryReply, pagetoken)
@ -211,31 +197,6 @@ func (h *HostHandler) GetHostsInfo(ctx *gin.Context) {
ReturnSuccess(ctx, common.SUCCESS_CODE, common.SUCCESS_MSG, queryResult) 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{} { func selectFields(host interface{}, fields []string) map[string]interface{} {
result := make(map[string]interface{}) result := make(map[string]interface{})
v := reflect.ValueOf(host) v := reflect.ValueOf(host)

View file

@ -7,53 +7,68 @@ import (
"apiserver/store" "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() store.InitStores()
// RUN job on t1. putJob := func(id uint64, execHosts string, status int) {
store.Jobs.Put(1, models.JobInfo{ store.Jobs.Put(id, models.JobInfo{
JobStatus: models.JOBSTAT["RUN"], JobStatus: status,
ExecHosts: models.Jsonb{String: `{"host_slots":[{"host_name":"t1","num_slots":1}],"num_hosts":1}`}, ExecHosts: models.Jsonb{String: execHosts},
}) })
// 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"], // Plain RUN job on t1 (1 slot).
ExecHosts: models.Jsonb{String: `{"host_slots":[{"host_name":"t2","num_slots":1}],"num_hosts":1}`}, 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).
// Multi-host RUN job: one job on t1 + t3 counts on both. putJob(2, `{"host_slots":[{"host_name":"t2","num_slots":1}],"num_hosts":1}`, models.JOBSTAT["RUN"]|models.JOBSTAT["USUSP"])
store.Jobs.Put(3, models.JobInfo{ // Multi-host RUN job: one job on t1 + t3 counts on both hosts.
JobStatus: models.JOBSTAT["RUN"], putJob(3, `{"host_slots":[{"host_name":"t1","num_slots":1},{"host_name":"t3","num_slots":1}],"num_hosts":2}`, models.JOBSTAT["RUN"])
ExecHosts: models.Jsonb{String: `{"host_slots":[{"host_name":"t1","num_slots":1},{"host_name":"t3","num_slots":1}],"num_hosts":2}`}, // 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. // PEND and DONE jobs must NOT count.
store.Jobs.Put(4, models.JobInfo{ putJob(6, `{"host_slots":[{"host_name":"t3","num_slots":1}],"num_hosts":1}`, models.JOBSTAT["PEND"])
JobStatus: models.JOBSTAT["PEND"], putJob(7, `{"host_slots":[{"host_name":"t1","num_slots":1}],"num_hosts":1}`, models.JOBSTAT["DONE"])
ExecHosts: models.Jsonb{String: `{"host_slots":[{"host_name":"t3","num_slots":1}],"num_hosts":1}`}, // RUN job with empty / bad exec_hosts must not crash or count.
}) putJob(8, ``, models.JOBSTAT["RUN"])
store.Jobs.Put(5, models.JobInfo{ putJob(9, `not-json`, models.JOBSTAT["RUN"])
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() counts := computeHostSlotUsage()
if counts["t1"] != 2 { // jobs 1 + 3 // t1: job1 (RUN, 1) + job3 (RUN, 1) = 2 RUN slots.
t.Errorf("t1 running = %d, want 2", counts["t1"]) if got := counts["t1"].Run; got != 2 {
t.Errorf("t1 Run = %d, want 2", got)
} }
if counts["t2"] != 1 { if got := counts["t1"].SSusp + counts["t1"].USusp; got != 0 {
t.Errorf("t2 running = %d, want 1", counts["t2"]) t.Errorf("t1 susp buckets = %d, want 0", got)
} }
if counts["t3"] != 1 { // job 3 only (job 4 is PEND) // t2: RUN|USUSP job must land in USusp, not Run.
t.Errorf("t3 running = %d, want 1", counts["t3"]) 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 { if _, ok := counts["nohost"]; ok {
t.Error("unexpected host 'nohost' in counts") t.Error("unexpected host 'nohost' in counts")