fix(apiserver): stop job-control panic on int/int64 type mismatch
xcecli jobs kill/stop/resume/checkpoint failed for every job with an empty error body. models.JobInfo.JobStatus/.ArrayIdx are plain Go `int`, but validateJobControl/foundJob asserted them as `int64` via a bare type assertion, which always panicked; Gin's Recovery() middleware swallowed the panic and returned an empty 500. Fixes gitea#45 / bug-1742. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y4M5MJM2zK6nU43da267Ku
This commit is contained in:
parent
83d40fbb33
commit
343639fc7e
3 changed files with 128 additions and 10 deletions
|
|
@ -3010,8 +3010,24 @@ func validateJobControl(jobinfo utils.OrderMap, action string,
|
|||
actionType = models.JOB_KILL
|
||||
}
|
||||
|
||||
// bug-1742: array_idx is a plain Go `int` on models.JobInfo, not `int64`
|
||||
// -- utils.IntOrInt64 accepts either, unlike a bare `.(int64)` assertion.
|
||||
arrayIdx, arrayOk := utils.IntOrInt64(jobinfo.Data["array_idx"])
|
||||
if !arrayOk {
|
||||
logger.Error("array_idx is not an int/int64", zap.Any("array_idx", jobinfo.Data["array_idx"]))
|
||||
failedJobs = append(failedJobs, map[string]interface{}{
|
||||
"unique_job_id": jobinfo.Data["unique_job_id"].(int64),
|
||||
"job_id": jobinfo.Data["job_id"].(int64),
|
||||
"array_idx": arrayIdx,
|
||||
"msg": fmt.Sprintf("Invalid array_idx"),
|
||||
})
|
||||
return successJobs, failedJobs
|
||||
}
|
||||
|
||||
if rawStatus, exists := jobinfo.Data["job_status"]; exists {
|
||||
if jobStatus, ok := rawStatus.(int64); ok {
|
||||
// bug-1742: job_status is likewise a plain Go `int`, not `int64`.
|
||||
if jobStatusInt, ok := utils.IntOrInt64(rawStatus); ok {
|
||||
jobStatus := int64(jobStatusInt)
|
||||
db_job_status := int(jobStatus)
|
||||
job_status := models.JobStateStr(db_job_status)
|
||||
jobStatus = int64(models.JOBSTAT[job_status])
|
||||
|
|
@ -3021,14 +3037,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": arrayIdx,
|
||||
"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": arrayIdx,
|
||||
"msg": fmt.Sprintf("job is being <%s>", action),
|
||||
})
|
||||
}
|
||||
|
|
@ -3037,7 +3053,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": arrayIdx,
|
||||
"msg": fmt.Sprintf("job is being <%s>", action),
|
||||
})
|
||||
} else {
|
||||
|
|
@ -3045,16 +3061,16 @@ 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": arrayIdx,
|
||||
"msg": fmt.Sprintf("job cannot be <%s>", action),
|
||||
})
|
||||
}
|
||||
} else {
|
||||
logger.Error("job_status is not an int64", zap.Any("job_status", rawStatus))
|
||||
logger.Error("job_status is not an int/int64", zap.Any("job_status", rawStatus))
|
||||
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": arrayIdx,
|
||||
"msg": fmt.Sprintf("Invalid job_status"),
|
||||
})
|
||||
}
|
||||
|
|
@ -3063,7 +3079,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": arrayIdx,
|
||||
"msg": fmt.Sprintf("job_status not found for job"),
|
||||
})
|
||||
}
|
||||
|
|
@ -3078,8 +3094,11 @@ func foundJob(jobid int64, array_idx int, jobinfos []utils.OrderMap) ([]utils.Or
|
|||
jobs := make([]utils.OrderMap, 0)
|
||||
for _, jobinfo := range jobinfos {
|
||||
if array_idx > 0 {
|
||||
if jobinfo.Data["job_id"].(int64) == jobid &&
|
||||
int(jobinfo.Data["array_idx"].(int64)) == array_idx {
|
||||
// bug-1742: array_idx is a plain Go `int` on models.JobInfo, not
|
||||
// `int64` -- utils.IntOrInt64 accepts either.
|
||||
jobinfoArrayIdx, ok := utils.IntOrInt64(jobinfo.Data["array_idx"])
|
||||
if ok && jobinfo.Data["job_id"].(int64) == jobid &&
|
||||
jobinfoArrayIdx == array_idx {
|
||||
jobs = append(jobs, jobinfo)
|
||||
}
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -119,3 +119,76 @@ func TestPreCheckJobRequest_EmptyQueue(t *testing.T) {
|
|||
t.Errorf("empty queue should not trigger reserved-name error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateJobControl_HandlesRealJobInfoTypes is a regression guard for
|
||||
// bug-1742 (gitea #45): models.JobInfo.JobStatus and .ArrayIdx are plain Go
|
||||
// `int` (see the JobInfo struct in models/jobs.go), but validateJobControl
|
||||
// asserted them as `int64` -- a mismatch invisible to any test that
|
||||
// hand-built its utils.OrderMap with int64 literals instead of going through
|
||||
// the same store.StructToOrderMap() reflection path production actually
|
||||
// uses. That mismatch made every job-control call panic with "interface
|
||||
// conversion: interface {} is int, not int64", surfaced to xcecli as an
|
||||
// empty `failed: []` (Gin's Recovery() middleware swallows the panic).
|
||||
func TestValidateJobControl_HandlesRealJobInfoTypes(t *testing.T) {
|
||||
resetJobSubmitState()
|
||||
|
||||
jobInfo := models.JobInfo{
|
||||
Unique_JobID: 73932643848159233,
|
||||
JobID: 1788186846,
|
||||
ArrayIdx: 1,
|
||||
SubmitUser: "admin",
|
||||
QueueName: "normal",
|
||||
JobStatus: models.JOBSTAT["RUN"],
|
||||
}
|
||||
orderMap := store.StructToOrderMap(jobInfo)
|
||||
|
||||
successJobs, failedJobs := validateJobControl(orderMap, "kill", "")
|
||||
|
||||
if len(successJobs) != 1 || len(failedJobs) != 0 {
|
||||
t.Fatalf("expected exactly one success job for a killable RUN job, got successJobs=%v failedJobs=%v", successJobs, failedJobs)
|
||||
}
|
||||
if got := successJobs[0]["array_idx"]; got != 1 {
|
||||
t.Errorf("array_idx = %v (%T), want 1 (int)", got, got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateJobControl_RejectsNonKillableStatus verifies a job in a
|
||||
// terminal state is reported as a failure (not silently dropped, and
|
||||
// without panicking) -- this path also reads array_idx before returning.
|
||||
func TestValidateJobControl_RejectsNonKillableStatus(t *testing.T) {
|
||||
resetJobSubmitState()
|
||||
|
||||
jobInfo := models.JobInfo{
|
||||
Unique_JobID: 1,
|
||||
JobID: 1,
|
||||
ArrayIdx: 0,
|
||||
JobStatus: models.JOBSTAT["DONE"],
|
||||
}
|
||||
orderMap := store.StructToOrderMap(jobInfo)
|
||||
|
||||
successJobs, failedJobs := validateJobControl(orderMap, "kill", "")
|
||||
|
||||
if len(successJobs) != 0 || len(failedJobs) != 1 {
|
||||
t.Fatalf("expected exactly one failed job for a DONE job, got successJobs=%v failedJobs=%v", successJobs, failedJobs)
|
||||
}
|
||||
}
|
||||
|
||||
// TestFoundJob_MatchesRealArrayIdxType is a regression guard for the same
|
||||
// bug-1742 type mismatch inside foundJob's array-index lookup path.
|
||||
func TestFoundJob_MatchesRealArrayIdxType(t *testing.T) {
|
||||
resetJobSubmitState()
|
||||
|
||||
jobInfo := models.JobInfo{
|
||||
JobID: 1788186846,
|
||||
ArrayIdx: 1,
|
||||
}
|
||||
orderMap := store.StructToOrderMap(jobInfo)
|
||||
|
||||
found, err := foundJob(1788186846, 1, []utils.OrderMap{orderMap})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if len(found) != 1 {
|
||||
t.Fatalf("expected to find 1 job, got %d", len(found))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue