| id |
status |
status_updated |
status_note |
component |
severity |
timestamp |
last_seen |
occurrences |
files |
tags |
related |
fix_commits |
| bug-1899 |
fixed |
2026-10-01 |
|
apiserver |
wrong-result |
2026-10-01T05:10:35Z |
2026-10-01T05:10:35Z |
1 |
| src/frontend/apiserver/controllers/jobs.go |
|
| apiserver |
| control |
| job-array |
| code-review |
|
|
|
bug-1899: ControlJobByID re-dispatches earlier selections on every later selection of one request
Description
Observed: found by code review, no failing run captured yet. In ControlJobByID the slice successJobs is declared once before the loop over the request's selections (src/frontend/apiserver/controllers/jobs.go:2464), is appended to inside that loop (:2575-2583), and the dispatch loop over the whole accumulated slice also sits inside it (:2594-2617):
successJobs := []map[string]interface{}{} // :2464, outside
for _, ujobFilterCdArray := range allFiltCdArrays { // one per selection
...
successJobs = append(successJobs, ...) // :2575
if len(successJobs) > 0 {
for _, successJob := range successJobs { // re-sends all earlier ones
err = backend.ControlJob(...) // :2611
Expected: each selected job receives the control action once per request; the response lists it once.
Impact: wrong result: xcecli jobs kill X[1],Y[2] (or kill A,B) signals the first selection's jobs once per later selection (N selections -> the first job is sent N times); a repeated kill of an already-killed job can make the backend call fail and the request report an error; BuildBatchOperationResponse receives the accumulated slice, so the response lists earlier jobs repeatedly. Distinct from the out-of-scope duplicate-signal case of one element selected twice (job-array-one-based-index requirements).
Found by: review (design review round 11 of spec job-array-one-based-index, 2026-10-01)
Environment
- OS: no /etc/os-release; Docker container; NixOS kernel; kernel Linux 6.18.33 x86_64
- libc: n/a (pure-Go control handler); host glibc 2.40
- Toolchain: gcc (GCC) 15.2.0, meson 1.9.1
- Build: asan (xce.build); defect is in pure Go, independent of profile
- Code: scheduler-framework @ 8f5ba635-dirty
- Deployment: any (single-node or HA) with at least two controllable jobs
Reproduction
- Read
src/frontend/apiserver/controllers/jobs.go:2464-2620 (ControlJobByID): the backend.ControlJob loop over successJobs is inside the per-selection loop.
- Live: submit two
sleep 300 jobs A and B; xcecli jobs stop A,B; the controller log shows the stop for A twice.
- Frequency: deterministic (by code reading; not yet run)
- Evidence: not kept (code reading only)
Root cause
In ControlJobByID (src/frontend/apiserver/controllers/jobs.go), successJobs was declared once outside the loop over the request's selections and appended to inside it, while the backend.ControlJob dispatch loop over the whole accumulated slice also sat inside the per-selection loop. Every later selection therefore re-dispatched all jobs collected for earlier selections, and the response was built from the accumulated slice.
Fix
Task 7.4 of spec job-array-one-based-index restructures the handler: all selections are validated before any store access; the selection loop only collects the jobs to act on (de-duplicated by unique_job_id) and the per-number not-found results; after the loop each job is validated once with validateJobControl and each accepted job is dispatched exactly once through controlJob.
Verification
- Test:
TestJobHandler_ControlTwoSelectionsDispatchOnce in src/frontend/apiserver/controllers/jobs_handler_test.go: RED before (X[1],Y[2] dispatched [x1 x1 x1 x1 y2 y2]; A,B dispatched [A A A A B B]), GREEN after ([x1 y2], [A B])
- Suites: apiserver non-cgo unit tests (controllers, store, utils, backend) and the cgo build via
xce-build.sh apiserver asan build.jai, all passing, 2026-10-01
- Live: n/a here; the live REST control flow is exercised in task 10.2 of the spec
References
- Related: bug-1898 (same review), bug-1896 (spec job-array-one-based-index restructures this handler)