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:
parent
801d26db06
commit
6a2c69f8bd
2 changed files with 158 additions and 45 deletions
|
|
@ -1522,6 +1522,131 @@ func TestJobHandler_RestartNamedJob(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// bug-1968: restart, like submit (bug-1936), refuses an option the restarted
|
||||
// job would never get: asked hosts (-m) have no field in the controller's
|
||||
// submit data. Nothing is submitted.
|
||||
func TestJobHandler_RestartRejectsAskedHosts(t *testing.T) {
|
||||
f := newHandlerFixture(t)
|
||||
ord := handlerTestOrdinaryBase + 50<<15
|
||||
f.seedRestartJob(ord, "sleep 1")
|
||||
|
||||
before := len(f.submitted)
|
||||
w := f.do(http.MethodPost, "/xce/v1/jobs/"+strconv.FormatUint(ord, 10)+"/restart",
|
||||
map[string]interface{}{"asked_hosts": "hostA"})
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("status = %d, want 400; body: %s", w.Code, w.Body.String())
|
||||
}
|
||||
if len(f.submitted) != before {
|
||||
t.Errorf("%d submits, want none", len(f.submitted)-before)
|
||||
}
|
||||
}
|
||||
|
||||
// bug-1968: the restarted job is rebuilt from the old job's row. Options no
|
||||
// job can get (asked hosts, begin/term time, I/O paths, out_dir; refused at
|
||||
// submit since bug-1936) must not be carried over, while the options the job
|
||||
// does get -- extended data (env, limits), cwd and exec home -- must be,
|
||||
// whether or not the old job was checkpointed.
|
||||
func TestJobHandler_RestartCarriesOnlyHonouredOptions(t *testing.T) {
|
||||
f := newHandlerFixture(t)
|
||||
ord := handlerTestOrdinaryBase + 51<<15
|
||||
exdata := `{"sub_env_vars":{"A":"1"},"limits":{"run_limit":60}}`
|
||||
f.seedJob(models.JobInfo{
|
||||
Unique_JobID: int64(ord),
|
||||
JobID: int64(backend.ExtractArrayBaseID(ord)),
|
||||
JobName: "r",
|
||||
Command: "sleep 1",
|
||||
JobStatus: models.JOBSTAT["DONE"],
|
||||
CWD: "/data/run",
|
||||
ExecHome: "/home/u",
|
||||
ResReq: "rusage[mem=100]",
|
||||
ExtendData: models.Jsonb{String: exdata},
|
||||
AskedHosts: "hostA",
|
||||
BeginTime: models.CustomTime{Time: time.Unix(1700000000, 0)},
|
||||
TermTime: models.CustomTime{Time: time.Unix(1800000000, 0)},
|
||||
InFile: "/in",
|
||||
OutFile: "/out",
|
||||
ErrFile: "/err",
|
||||
OutDir: "/outdir",
|
||||
})
|
||||
|
||||
before := len(f.submitted)
|
||||
w := f.restart(strconv.FormatUint(ord, 10))
|
||||
if w.Code != http.StatusOK && w.Code != http.StatusCreated {
|
||||
t.Fatalf("status = %d; body: %s", w.Code, w.Body.String())
|
||||
}
|
||||
if len(f.submitted) != before+1 {
|
||||
t.Fatalf("%d submits, want 1", len(f.submitted)-before)
|
||||
}
|
||||
job := f.submitted[before].Job
|
||||
|
||||
dropped := map[string]bool{
|
||||
"asked_hosts": job.AskedHosts != "",
|
||||
"begin_time": !job.BeginTime.Time.IsZero(),
|
||||
"term_time": !job.TermTime.Time.IsZero(),
|
||||
"in_file": job.InFile != "",
|
||||
"out_file": job.OutFile != "",
|
||||
"err_file": job.ErrFile != "",
|
||||
"out_dir": job.OutDir != "",
|
||||
}
|
||||
for field, kept := range dropped {
|
||||
if kept {
|
||||
t.Errorf("restarted job kept %s, which no job gets", field)
|
||||
}
|
||||
}
|
||||
if job.ExtendData.String != exdata {
|
||||
t.Errorf("ExtendData = %q, want %q", job.ExtendData.String, exdata)
|
||||
}
|
||||
if job.CWD != "/data/run" {
|
||||
t.Errorf("CWD = %q, want /data/run", job.CWD)
|
||||
}
|
||||
if job.ExecHome != "/home/u" {
|
||||
t.Errorf("ExecHome = %q, want /home/u", job.ExecHome)
|
||||
}
|
||||
if job.ResReq != "rusage[mem=100]" {
|
||||
t.Errorf("ResReq = %q, want rusage[mem=100]", job.ResReq)
|
||||
}
|
||||
}
|
||||
|
||||
// bug-1968: a checkpointed job restarts from its checkpoint: the restart
|
||||
// command replaces the command and the checkpointed pid reaches the job as
|
||||
// XCE_JOB_CHECKPOINT_PID next to the old job's environment. The stored
|
||||
// chkpnt_info/extend_data are models.Jsonb, which the restart never read.
|
||||
func TestJobHandler_RestartFromCheckpoint(t *testing.T) {
|
||||
f := newHandlerFixture(t)
|
||||
ord := handlerTestOrdinaryBase + 52<<15
|
||||
f.seedJob(models.JobInfo{
|
||||
Unique_JobID: int64(ord),
|
||||
JobID: int64(backend.ExtractArrayBaseID(ord)),
|
||||
JobName: "r",
|
||||
Command: "sleep 1",
|
||||
JobStatus: models.JOBSTAT["EXIT"],
|
||||
ChkpntInfo: models.Jsonb{String: `{"restart_command":"criu restore","ckpt_pid":4321}`},
|
||||
ExtendData: models.Jsonb{String: `{"sub_env_vars":{"A":"1"}}`},
|
||||
})
|
||||
|
||||
before := len(f.submitted)
|
||||
w := f.restart(strconv.FormatUint(ord, 10))
|
||||
if w.Code != http.StatusOK && w.Code != http.StatusCreated {
|
||||
t.Fatalf("status = %d; body: %s", w.Code, w.Body.String())
|
||||
}
|
||||
if len(f.submitted) != before+1 {
|
||||
t.Fatalf("%d submits, want 1", len(f.submitted)-before)
|
||||
}
|
||||
job := f.submitted[before].Job
|
||||
if job.Command != "criu restore" {
|
||||
t.Errorf("Command = %q, want the checkpoint's restart command", job.Command)
|
||||
}
|
||||
var ex struct {
|
||||
Env map[string]string `json:"sub_env_vars"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(job.ExtendData.String), &ex); err != nil {
|
||||
t.Fatalf("ExtendData %q: %v", job.ExtendData.String, err)
|
||||
}
|
||||
if ex.Env["A"] != "1" || ex.Env["XCE_JOB_CHECKPOINT_PID"] != "4321" {
|
||||
t.Errorf("sub_env_vars = %v, want A=1 and XCE_JOB_CHECKPOINT_PID=4321", ex.Env)
|
||||
}
|
||||
}
|
||||
|
||||
// bug-1936: the job handed to the backend carries the cwd and the extended
|
||||
// data under the key names and units the worker's parse_exdata()/
|
||||
// parse_limits() read (run_limit in seconds, memory limits in MB,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue