fix(apiserver): validate, default and document job.terminate_interval (bug-1957)

The loader now stores the effective interval (10 s when unset or 0, LSF's
JOB_TERMINATE_INTERVAL default) and refuses a negative one; app_schema.json
describes the setting; src/docs/xce.md documents kill/stop behaviour and
the interval.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Rongsong Shen 2026-10-10 10:04:47 +00:00
parent 73fe8f787c
commit 97f7c800f1
Signed by untrusted user: rshen
GPG key ID: 80533536B0EB651C
4 changed files with 59 additions and 0 deletions

View file

@ -54,6 +54,23 @@ cluster. Common operations:
Built as a standalone Go binary; communicates with the API server over Built as a standalone Go binary; communicates with the API server over
its REST interface. its REST interface.
## Killing and stopping jobs
Job control follows LSF's TERMINATE and SUSPEND actions:
- **Kill** (`xcecli jobs kill <id>`, no signal given): SIGINT, then SIGTERM,
then SIGKILL, one interval apart, each only while the job is still
running, so the job can catch SIGINT/SIGTERM and clean up.
- **Stop** (`xcecli jobs stop <id>`): SIGTSTP first, so the job can catch
it, prepare and stop itself; SIGSTOP follows after the interval if it has
not stopped. **Resume** sends SIGCONT and cancels a stop still in progress.
- **Explicit signal** (kill with a signal): that signal only, once.
- **Force kill**: SIGKILL at once.
The interval is the API server's `job.terminate_interval` setting
(`config/app.yaml`, seconds; default 10 when unset or 0, as LSF's
`JOB_TERMINATE_INTERVAL`; negative refuses startup).
## Job arrays ## Job arrays
A job array is a set of identical jobs (elements) submitted at once. XCE A job array is a set of identical jobs (elements) submitted at once. XCE

View file

@ -112,6 +112,11 @@
"minimum": 1, "minimum": 1,
"maximum": 2147483646, "maximum": 2147483646,
"description": "Default max array size for job submission" "description": "Default max array size for job submission"
},
"terminate_interval": {
"type": "integer",
"minimum": 0,
"description": "Seconds between the signals of a plain job kill (SIGINT, SIGTERM, SIGKILL) or stop (SIGTSTP, SIGSTOP); 0 or unset means 10 (LSF JOB_TERMINATE_INTERVAL)"
} }
} }
}, },

View file

@ -247,6 +247,9 @@ func InitConf() error {
if terminateInterval < 0 { if terminateInterval < 0 {
return fmt.Errorf("job.terminate_interval must not be negative: %d", terminateInterval) return fmt.Errorf("job.terminate_interval must not be negative: %d", terminateInterval)
} }
if terminateInterval == 0 {
terminateInterval = defaultTerminateInterval
}
JobYamlConfig = &JobConfig{ JobYamlConfig = &JobConfig{
MaxArraySize: maxArraySize, MaxArraySize: maxArraySize,
TerminateInterval: terminateInterval, TerminateInterval: terminateInterval,
@ -316,6 +319,9 @@ func ParseMaxSize(maxSize string) (int64, error) {
} }
const ( const (
// defaultTerminateInterval is used when job.terminate_interval is unset
// or 0: LSF's JOB_TERMINATE_INTERVAL default (bug-1957).
defaultTerminateInterval = 10
// defaultMaxArraySize is used when job.max_array_size is unset or 0. // defaultMaxArraySize is used when job.max_array_size is unset or 0.
defaultMaxArraySize = 1000 defaultMaxArraySize = 1000
// supportedMaxArraySize is the largest array the API server creates // supportedMaxArraySize is the largest array the API server creates

View file

@ -299,3 +299,34 @@ func TestInitConf_MaxArraySizeNegativeRefused(t *testing.T) {
t.Fatalf("InitConf with -1: expected job.max_array_size error, got %v", err) t.Fatalf("InitConf with -1: expected job.max_array_size error, got %v", err)
} }
} }
// bug-1957: job.terminate_interval, the seconds between the signals of a
// plain kill or stop (LSF JOB_TERMINATE_INTERVAL)
func TestInitConf_TerminateInterval(t *testing.T) {
setupInitConf(t, "job:\n terminate_interval: 30")
if err := InitConf(); err != nil {
t.Fatalf("InitConf: %v", err)
}
if JobYamlConfig.TerminateInterval != 30 {
t.Errorf("TerminateInterval = %d, want 30", JobYamlConfig.TerminateInterval)
}
}
func TestInitConf_TerminateIntervalUnsetDefaults(t *testing.T) {
for _, section := range []string{"", "job:\n terminate_interval: 0"} {
setupInitConf(t, section)
if err := InitConf(); err != nil {
t.Fatalf("%q: InitConf: %v", section, err)
}
if JobYamlConfig.TerminateInterval != 10 {
t.Errorf("%q: TerminateInterval = %d, want the default 10", section, JobYamlConfig.TerminateInterval)
}
}
}
func TestInitConf_TerminateIntervalNegativeRefused(t *testing.T) {
setupInitConf(t, "job:\n terminate_interval: -1")
if err := InitConf(); err == nil || !strings.Contains(err.Error(), "terminate_interval") {
t.Fatalf("InitConf with a negative interval: err = %v, want an error naming terminate_interval", err)
}
}