fix(apiserver): send the requested signal, force kill and checkpoint options (bug-1940)

Cause: ControlJob (backend/clib.go) looked its content argument up as a
signal name, but the job control handlers pass a JSON JobControlEvent, so
the lookup always missed and every kill sent TERM (stop TSTP, resume
CONT); sig.force and ckpt.kill were hard-coded to 0 and the checkpoint
method was never set.

Fix: new pure jobCtrlParams (backend/convert_pure.go) decodes the event
(or a bare signal name/number), validates the signal against
JobSignalMapTable and returns an error instead of falling back, and maps
force_kill to xce_signal_t.force (kill) or xce_ckpt_options_t.kill
(checkpoint) plus the checkpoint method. ControlJob fills the C structs
from it. Tests: backend/jobctrl_params_test.go.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Rongsong Shen 2026-10-10 08:31:16 +00:00
parent 8f164b8205
commit 4d2e80c4bb
Signed by untrusted user: rshen
GPG key ID: 80533536B0EB651C
3 changed files with 236 additions and 25 deletions

View file

@ -680,8 +680,9 @@ func SubmitJob(job *models.JobInfo, arraySize int) error {
}
// ControlJob sends a job control action to the backend via xcecli_jobctrl.
// action must be one of the models.JOB_* constants; content is an optional
// signal name (e.g. "TERM") for kill operations.
// action must be one of the models.JOB_* constants; content is the
// JSON-encoded models.JobControlEvent (signal, force_kill, method) or a bare
// signal name/number -- see jobCtrlParams (convert_pure.go).
func ControlJob(jobid uint64, action int, content string) error {
var ctrl C.xce_jobctrl_t
ctrl.jobid = C.uint64_t(jobid)
@ -693,19 +694,28 @@ func ControlJob(jobid uint64, action int, content string) error {
// For signal-based actions set signo; for checkpoint set ckpt options.
// xce_jobctrl_t.u is a C union — we use a static C helper to set the
// right branch safely.
args, err := jobCtrlParams(action, content)
if err != nil {
return err
}
switch action {
case models.JOB_STOP, models.JOB_RESUME, models.JOB_KILL:
var sig C.xce_signal_t
sig.signo = C.int(signalNumber(content, action))
sig.force = 0
sig.signo = C.int(args.Signo)
sig.force = C.int(args.Force)
C.jobctrl_set_siginfo(&ctrl, &sig)
case models.JOB_CHKPNT:
var ckpt C.xce_ckpt_options_t
ckpt.kill = 0
ckpt.kill = C.int(args.CkptKill)
if args.Method != "" {
// NULL makes xce_jobctrl_req() send "default"
ckpt.method = C.CString(args.Method)
defer C.free(unsafe.Pointer(ckpt.method))
}
C.jobctrl_set_ckptoption(&ctrl, &ckpt)
}
_, err := callBackend("xcecli_jobctrl", func() int {
_, err = callBackend("xcecli_jobctrl", func() int {
return int(C.xcecli_jobctrl(gCtx.ctx, &ctrl))
})
return err
@ -744,25 +754,6 @@ func ControlHost(hostname string, action int) error {
return err
}
// signalNumber maps a job action and optional signal name to a POSIX signal
// number, falling back to sensible defaults.
func signalNumber(signame string, action int) int {
if signame != "" {
if n, ok := models.JobSignalMapTable[signame]; ok {
return n
}
}
switch action {
case models.JOB_KILL:
return models.JobSignalMapTable["TERM"] // 15
case models.JOB_STOP:
return models.JobSignalMapTable["TSTP"] // 20
case models.JOB_RESUME:
return models.JobSignalMapTable["CONT"] // 18
}
return 15 // SIGTERM as safe default
}
// FetchFile fetches up to maxBytes of a job output file from offset via the
// controller→worker chain. The filepath must be the fully resolved absolute
// path on the exec host.