fix(apiserver): restart carries only the options a job gets (bug-1968)

RestartJobByID rebuilt the new job from the old job's row:
- it accepted asked_hosts and copied asked hosts, I/O paths and out_dir,
  which no job gets (refused at submit since bug-1936); restart now
  refuses asked_hosts with 400 and no longer carries those over;
- it read the JSONB columns chkpnt_info/extend_data as strings, but rows
  keep models.Jsonb, so env/limits were lost and a checkpointed job never
  restarted from its checkpoint; new rowJSON() reads both;
- it read "res_req" (the column is "resreq"), losing the resource
  requirement, and never copied exec_home.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Rongsong Shen 2026-10-10 14:23:16 +00:00
parent 801d26db06
commit 6a2c69f8bd
Signed by untrusted user: rshen
GPG key ID: 80533536B0EB651C
2 changed files with 158 additions and 45 deletions

View file

@ -857,7 +857,7 @@ func (j JobController) RestartJobByID(ctx *gin.Context) {
err = preCheckJobRestartRequest(&req)
if err != nil {
logger.Error("Failed to restart job", zap.Error(err))
ReturnError(ctx, common.ERROR_CODE, err.Error(), nil)
ReturnError(ctx, common.INVALID_PARAMS_CODE, err.Error(), nil)
return
}
@ -918,18 +918,11 @@ func (j JobController) RestartJobByID(ctx *gin.Context) {
JobStatus: models.JOBSTAT["PEND"],
}
err = processJobRestartRequest(&newJob, &req)
if err != nil {
logger.Error("Failed to restart job", zap.Error(err))
ReturnError(ctx, common.ERROR_CODE, err.Error(), nil)
return
}
if v, ok := oldJob["job_name"].(string); ok {
newJob.JobName = v
}
if chkpntStr, ok := oldJob["chkpnt_info"].(string); ok && chkpntStr != "" {
if chkpntStr := rowJSON(oldJob["chkpnt_info"]); chkpntStr != "" {
var chkpntInfo map[string]interface{}
if err := json.Unmarshal([]byte(chkpntStr), &chkpntInfo); err == nil {
if restartCmd, ok := chkpntInfo["restart_command"].(string); ok {
@ -938,7 +931,7 @@ func (j JobController) RestartJobByID(ctx *gin.Context) {
}
if ckpt_pid, ok := chkpntInfo["ckpt_pid"].(float64); ok {
extendData := map[string]interface{}{}
if v, ok := oldJob["extend_data"].(string); ok && v != "" {
if v := rowJSON(oldJob["extend_data"]); v != "" {
_ = json.Unmarshal([]byte(v), &extendData)
}
if v, ok := extendData["sub_env_vars"]; ok && v != nil {
@ -953,10 +946,6 @@ func (j JobController) RestartJobByID(ctx *gin.Context) {
}
extendDataBytes, _ := json.Marshal(extendData)
newJob.ExtendData = models.Jsonb{String: string(extendDataBytes)}
} else {
if v, ok := oldJob["extend_data"].(string); ok && v != "" {
newJob.ExtendData = models.Jsonb{String: v}
}
}
}
} else {
@ -984,7 +973,7 @@ func (j JobController) RestartJobByID(ctx *gin.Context) {
if v, ok := oldJob["job_description"].(string); ok {
newJob.JobDescription = v
}
if v, ok := oldJob["res_req"].(string); ok {
if v, ok := oldJob["resreq"].(string); ok {
newJob.ResReq = v
}
if v, ok := oldJob["rsv_id"].(string); ok {
@ -999,17 +988,6 @@ func (j JobController) RestartJobByID(ctx *gin.Context) {
if v, ok := oldJob["num_processors"].(int); ok {
newJob.NumProcessors = v
}
if newJob.AskedHosts == "" {
if v, ok := oldJob["asked_hosts"].(string); ok {
newJob.AskedHosts = v
}
}
if v, ok := oldJob["begin_time"].(time.Time); ok {
newJob.BeginTime = models.CustomTime{Time: v}
}
if v, ok := oldJob["term_time"].(time.Time); ok {
newJob.TermTime = models.CustomTime{Time: v}
}
if v, ok := oldJob["job_priority"].(int); ok {
newJob.JobPriority = v
}
@ -1046,17 +1024,17 @@ func (j JobController) RestartJobByID(ctx *gin.Context) {
if v, ok := oldJob["chkpnt_dir"].(string); ok {
newJob.ChkpntDir = v
}
if v, ok := oldJob["out_dir"].(string); ok {
newJob.OutDir = v
// bug-1968: asked hosts, begin/term time, in/out/err files and out_dir
// are not carried over: no job gets them (refused at submit since
// bug-1936), so the restarted job would only show them. Extended data
// (env, limits) and exec home are, checkpointed or not.
if newJob.ExtendData.String == "" {
if v := rowJSON(oldJob["extend_data"]); v != "" {
newJob.ExtendData = models.Jsonb{String: v}
}
}
if v, ok := oldJob["in_file"].(string); ok {
newJob.InFile = v
}
if v, ok := oldJob["err_file"].(string); ok {
newJob.ErrFile = v
}
if v, ok := oldJob["out_file"].(string); ok {
newJob.OutFile = v
if v, ok := oldJob["exec_home"].(string); ok {
newJob.ExecHome = v
}
if v, ok := oldJob["login_shell"].(string); ok {
newJob.LoginShell = v
@ -1347,14 +1325,31 @@ func preCheckSubmitOptions(req *models.JobSubmitRequest) error {
return nil
}
// preCheckJobRestartRequest refuses a restart option the restarted job would
// not get (bug-1968, as preCheckSubmitOptions does for submit, bug-1936):
// asked hosts have no field in the controller's submit data.
func preCheckJobRestartRequest(req *models.JobRestartRequest) error {
logger.Debug("Enter preCheckJobRestartRequest")
if err := validateAskedHosts(req.AskedHosts); err != nil {
return err
if strings.TrimSpace(req.AskedHosts) != "" {
return fmt.Errorf("job restart option asked_hosts not supported: the job would not get it")
}
return nil
}
// rowJSON returns a JSON column of a stored job row as a string: rows come
// from store.StructToOrderMap, which keeps each field's Go type, so a JSONB
// column (chkpnt_info, extend_data) is a models.Jsonb, not a string
// (bug-1968).
func rowJSON(v interface{}) string {
switch j := v.(type) {
case models.Jsonb:
return j.String
case string:
return j
}
return ""
}
// validateResreqSyntax is the resource-request syntax check used by
// preCheckJobRequest; a variable so tests can substitute it (bug-1744).
var validateResreqSyntax = backend.ValidateResreqSyntax
@ -1909,13 +1904,6 @@ func parseTransferFiles(transferFileStr string) (localFile, operator, remoteFile
return localFile, operator, remoteFile, nil
}
func processJobRestartRequest(newJob *models.JobInfo, req *models.JobRestartRequest) error {
logger.Debug("Enter processJobRestartRequest function", zap.Any("req", req))
if req.AskedHosts != "" {
newJob.AskedHosts = req.AskedHosts
}
return nil
}
func processJobRequest(ctx *gin.Context, req *models.JobSubmitRequest, defaultValues map[string]string) error {
if req.InFile != "" {