fix(frontend): job control no longer returns HTTP 500 (bug-1490)

Two stacked defects in the apiserver job-control link, both of which kept
hold/release/cancel from ever reaching the controller:

1. controllers/jobs.go: validateJobControl and foundJob asserted
   jobinfo.Data["array_idx"].(int64), but StoreToOrderMaps produces
   array_idx as int (models.JobInfo.ArrayIdx is declared int) -> panic
   -> Gin Recovery -> HTTP 500 before backend.ControlJob ran. Add
   normalizeJobInt64 / jobArrayIdx helpers and use them in both
   functions (job_status already had this fix via normalizeJobStatus;
   array_idx was missed).

2. backend/clib.go: backend.ControlJob set xce_jobctrl_t.action to the
   models.JOB_* constant (JOB_STOP=1 / JOB_RESUME=2 / JOB_KILL=3 /
   JOB_CHKPNT=8), but that field is the capnp JobControl union
   discriminant (JobControl_u_siginfo=0 / JobControl_u_ckptoption=1).
   The controller's job_ctrl() selected the wrong union branch (or
   none, since encode_JobControl drops the union pointer for
   non-0/1 discriminants) and rejected every control with -EINVAL.
   Add jobCtrlDiscriminant() and use it.

Also: resolveExecUser was defined in the cgo-only convert.go but is
exercised by a non-cgo test, so `make test` (CGO_ENABLED=0) failed to
build the backend package. Move it to convert_pure.go so it compiles in
both modes.

Tests: 3 controller regression tests (validateJobControl allowed /
not-allowed branches, foundJob array-child branch) + 1 backend
jobCtrlDiscriminant test. make test fully green. Cluster E2E verified:
hold -> PSUSP, release -> PEND, cancel -> EXIT on a PEND job, no 500,
no -EINVAL, all daemons healthy.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
lei.chen 2026-08-07 07:35:51 +00:00
parent d3a92f1033
commit c90e63ec0f
7 changed files with 368 additions and 29 deletions

View file

@ -3025,14 +3025,14 @@ func validateJobControl(jobinfo utils.OrderMap, action string,
failedJobs = append(failedJobs, map[string]interface{}{
"unique_job_id": jobinfo.Data["unique_job_id"].(int64),
"job_id": jobinfo.Data["job_id"].(int64),
"array_idx": int(jobinfo.Data["array_idx"].(int64)),
"array_idx": jobArrayIdx(jobinfo),
"msg": fmt.Sprintf("job cannot be <killed> or <signaled>"),
})
} else {
successJobs = append(successJobs, map[string]interface{}{
"unique_job_id": jobinfo.Data["unique_job_id"].(int64),
"job_id": jobinfo.Data["job_id"].(int64),
"array_idx": int(jobinfo.Data["array_idx"].(int64)),
"array_idx": jobArrayIdx(jobinfo),
"msg": fmt.Sprintf("job is being <%s>", action),
})
}
@ -3041,7 +3041,7 @@ func validateJobControl(jobinfo utils.OrderMap, action string,
successJobs = append(successJobs, map[string]interface{}{
"unique_job_id": jobinfo.Data["unique_job_id"].(int64),
"job_id": jobinfo.Data["job_id"].(int64),
"array_idx": int(jobinfo.Data["array_idx"].(int64)),
"array_idx": jobArrayIdx(jobinfo),
"msg": fmt.Sprintf("job is being <%s>", action),
})
} else {
@ -3049,7 +3049,7 @@ func validateJobControl(jobinfo utils.OrderMap, action string,
failedJobs = append(failedJobs, map[string]interface{}{
"unique_job_id": jobinfo.Data["unique_job_id"].(int64),
"job_id": jobinfo.Data["job_id"].(int64),
"array_idx": int(jobinfo.Data["array_idx"].(int64)),
"array_idx": jobArrayIdx(jobinfo),
"msg": fmt.Sprintf("job cannot be <%s>", action),
})
}
@ -3058,7 +3058,7 @@ func validateJobControl(jobinfo utils.OrderMap, action string,
failedJobs = append(failedJobs, map[string]interface{}{
"unique_job_id": jobinfo.Data["unique_job_id"].(int64),
"job_id": jobinfo.Data["job_id"].(int64),
"array_idx": int(jobinfo.Data["array_idx"].(int64)),
"array_idx": jobArrayIdx(jobinfo),
"msg": fmt.Sprintf("Invalid job_status"),
})
}
@ -3067,7 +3067,7 @@ func validateJobControl(jobinfo utils.OrderMap, action string,
failedJobs = append(failedJobs, map[string]interface{}{
"unique_job_id": jobinfo.Data["unique_job_id"].(int64),
"job_id": jobinfo.Data["job_id"].(int64),
"array_idx": int(jobinfo.Data["array_idx"].(int64)),
"array_idx": jobArrayIdx(jobinfo),
"msg": fmt.Sprintf("job_status not found for job"),
})
}
@ -3083,7 +3083,7 @@ func foundJob(jobid int64, array_idx int, jobinfos []utils.OrderMap) ([]utils.Or
for _, jobinfo := range jobinfos {
if array_idx > 0 {
if jobinfo.Data["job_id"].(int64) == jobid &&
int(jobinfo.Data["array_idx"].(int64)) == array_idx {
jobArrayIdx(jobinfo) == array_idx {
jobs = append(jobs, jobinfo)
}
} else {
@ -3099,26 +3099,48 @@ func foundJob(jobid int64, array_idx int, jobinfos []utils.OrderMap) ([]utils.Or
}
}
// normalizeJobStatus converts a job_status value (int, int64, or float64 from
// JSON decode) into a plain int. Returns ok=false for unsupported types.
func normalizeJobStatus(v any) (int, bool) {
// normalizeJobInt64 converts a job field value (int, int32, int64, float64, or
// json.Number — whichever representation the store or a JSON decode path
// produced) into a plain int64. Returns ok=false for unsupported types.
func normalizeJobInt64(v any) (int64, bool) {
switch s := v.(type) {
case int:
return s, true
return int64(s), true
case int32:
return int(s), true
return int64(s), true
case int64:
return int(s), true
return s, true
case float64:
return int(s), true
return int64(s), true
case json.Number:
if n, err := s.Int64(); err == nil {
return int(n), true
return n, true
}
}
return 0, false
}
// normalizeJobStatus converts a job_status value (int, int64, or float64 from
// JSON decode) into a plain int. Returns ok=false for unsupported types.
func normalizeJobStatus(v any) (int, bool) {
n, ok := normalizeJobInt64(v)
return int(n), ok
}
// jobArrayIdx extracts the array_idx field from a job row's Data map, tolerating
// any numeric representation the store produces. Defaults to 0 when absent.
// Regression for bug-1490: the old code asserted .(int64) on a value the store
// produces as int, panicking every control request into an HTTP 500 before it
// reached the controller.
func jobArrayIdx(row utils.OrderMap) int {
if v, ok := row.Data["array_idx"]; ok {
if n, ok := normalizeJobInt64(v); ok {
return int(n)
}
}
return 0
}
func processJobData(rows []utils.OrderMap, pagetoken string) utils.QueryResult {
logger.Debug("Enter processJobData() function", zap.Any("rows", rows))
for _, row := range rows {