Spec for numbering job-array elements from 1 (bug-1896): approved requirements, a design reviewed over 20 rounds until clean (log in research.md), and a 30-task plan. The design review found two live apiserver defects, logged here: bracketed job_id/job_name filters are never evaluated (bug-1898) and ControlJobByID re-dispatches earlier selections (bug-1899). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
80 KiB
Design Document: job-array-one-based-index
Overview
Purpose: This feature numbers job-array elements from 1, as LSF does. Elements are numbered 1 to N everywhere they are created, shown, selected, controlled and exposed to the running job. Every index form the feature does not support is rejected with a specific message instead of being silently renumbered (bug-1896).
Users: Users submitting arrays through xcecli or REST, operators controlling and listing elements, developers driving the xce_test harness, and job scripts reading XCE_JOBINDEX.
Impact: The element number already lives in the 15-bit index field of the 64-bit job ID. Under option B (requirements decision 1), an array's first element ID becomes array-id | 1. The existing contiguous expansion first + i in the controller, WAL apply and scheduler then yields elements 1 to N, with no change to the wire, WAL, snapshot or recording formats. The design also bounds every job-identity parser in the system, and adds cluster-side validation of array submissions.
Goals
name[1-N],name[1]and harnessjobsub -a Ncreate elements 1 to N. Every other index form is rejected with the message its requirement names.- One bounded identity and selection grammar, with one check order (5.12), per language: C (libxce), Go apiserver, Go xcecli.
- The controller refuses array submissions it cannot represent. It also detects array-identity collisions, including against concurrent submissions.
- A running element sees its own number in
XCE_JOBINDEX, and the submission cannot override it. - Arrays submitted before this change keep working, including their element 0.
Non-Goals
- Starts other than 1 at the clients, comma lists, steps,
%Nlimits (all rejected, not implemented). - Per-element
%I/%Jexpansion (blocked on bug-1508), and LSFLSB_JOBINDEX/LSB_JOBID. - Whole-array control through the harness or at the controller; job output (peek) and restart by identity; the array statistics view.
- The
idgensequence overflow in general. ThepreCheckJobRequestearly return for named jobs (unchanged; the new 2.1 interactive-array check runs insideParseSubmitIndexExpr, before it, while today's interactive-value check atjobs.go:1734-1736stays where it is). All other defects listed as out of scope in the requirements.
Boundary Commitments
This Spec Owns
- Job identity grammar and bounds: the written form
c-a-ts-seq, with an optional[k]or[a-b], and the decimal form. The libxce, apiserver and xcecli parsers for them, and their check order. - Array ID allocation:
idgen_arrayand its Go wrapper.idgenreduced to ordinary IDs only. - Array-submission validation and collision detection in the controller's
handle_jobsubmit, including the in-flight registry's array-identity awareness. - The apiserver's handling of submit index expressions, the submit response shape (
jobid,array_size), REST selection parsing and matching, theunique_job_idof not-found control results, and the array-size limit setting. ControlJobByID's dispatch loop (bug-1899: moved after the selection loop, de-duplicated).- Evaluation of selection predicates in the apiserver's in-memory query pipeline (
store.ExecuteQuery). Today bracketed selections are built asCompConditionsthat are never evaluated (bug-1898); this spec replaces them with evaluated row predicates. - xcecli identity parsing, ID-list splitting, submit output and help text.
- Harness:
jobsub -a,$jobidand$jobid[...], the shared single-job argument helper, andexpect-jobmatching. - The worker's
XCE_JOBINDEX. - Scheduler: the array-expansion guard and its log line.
- Skill scripts,
src/docs/xce.md, and the cross-spec document updates listed in the File Structure Plan.
Out of Boundary
- Storage, replication and apply: WAL
JOB_ARRAY_SUBMITappend, apply and replay; snapshots; HA replication and standby apply; controller-to-scheduler and controller-to-apiserver pushes; scheduler recording and replay. They are relied on unchanged. - Display helpers:
fmt_jobid, the GoFormatJobIDs, andarray_jobid/array_idx(display semantics unchanged). RecordAcceptedJob. (The apiserver'sarray_idx betweenconstruction and the unevaluatedCompConditionsare replaced on the selection paths this spec owns, see ApiMatch; otherCompConditionsusers, if any appear, are untouched.)- Peek (
output.go,parseJobIDParam, the sixthparseArrayNamecaller; xceclipeek.go, which keeps a legacyToAPIJobIDLegacywith today's passthrough behaviour) and restart (RestartJobByID), which keepidgenwith size 1. - The scheduler REPL
info jobargument.
Allowed Dependencies
- libxce (
utils.c,idgen.c,errnox.h,config.h) is the lowest layer. The controller, harness, worker and scheduler depend on it. The apiserver reaches it only throughbackend/clib.gocgo, plus pure-Go mirrors inbackend/convert_pure.go. - The apiserver's selection parsers (
ParseIdentitySelection,ParseNameSelectionincontrollers/jobsel.go) depend only onbackendID helpers and the standard library. Predicate building (ApiMatch, incontrollers/jobs.go) may additionally useutils(BuildFilterConditions,OrderMap,QueryOptions) andstore(MatchesCondition). Neitherutilsnorstoredepends on controllers. - The xcecli
formatpackage stays dependency-free;cmd/andfilters/depend on it. - The harness helpers in
cmdutil.candautovars.cdepend on libxce only;test.cdepends on them. - No new third-party dependencies.
Revalidation Triggers
- Job ID bit layout (
config.h, the Go constants): any change invalidates all three parsers andidgen_array. - Controller submit reply semantics (still the first element ID).
clib.go,RecordAcceptedJoband the harness ack check depend on it. - REST response shapes:
JobSubmitResponsegainsarray_size;jobidnow names the array. Not-found control result items gainunique_job_id(found items already carry it). Any REST consumer (xcecli, the regression suite) must re-check. utils.QueryOptionsshape: gainsPredicates; any other query builder must re-check.- The new controller error status
E_XCE_ARRAY_START0, which every client error printer must know about. - Specs that defined
$jobidor element numbering:xce-test-e2e-primitivesandjob-array-single-wal-recordRequirement 5 are superseded here.
Architecture
Existing Architecture Analysis
- Submission: the client (apiserver or harness) allocates IDs with
idgen. It sends(first_jobid, array_size, submit)throughOPCode_jobsub. The controller checks for duplicates, commits oneJOB_ARRAY_SUBMITWAL record, and expandsfirst + ion apply. It replies withfirst_jobid. The scheduler expands the pushed array indriver.ctranslation, before core events. - Identity text is parsed in about ten places with differing leniency (research.md, "Discovery corrections").
- Constraints kept:
- The controller vtask handler never blocks (
ha-async-io.md). - Controllers never self-exit.
- REST handlers use
ReturnError(...); return. - The middleware chain is unchanged.
- The scheduler driver state-equivalence holds, because expansion stays before core events in
driver.conly.
- The controller vtask handler never blocks (
Architecture Pattern & Boundary Map
graph TB
subgraph Clients
Xcecli[xcecli format jobsel]
Harness[harness cmdutil autovars]
end
subgraph ApiServer
ApiSel[controllers jobsel]
ApiSubmit[controllers jobarray submit]
Clib[backend clib idgen wrapper]
end
subgraph Libxce
Parse[parse jobsel]
Idgen[idgen array]
end
subgraph Daemons
Ctrl[controller handle jobsubmit]
Sched[scheduler driver expansion]
Worker[worker exdata env]
end
Xcecli --> ApiSel
Xcecli --> ApiSubmit
ApiSubmit --> Clib
Clib --> Idgen
Clib --> Ctrl
ApiSel --> Ctrl
Harness --> Parse
Harness --> Idgen
Harness --> Ctrl
Ctrl --> Sched
Sched --> Worker
Architecture Integration:
- Selected pattern: one bounded parser per language behind each surface, plus a single validation gate at the controller.
- Domain boundaries: identity text belongs to the parsers. Admission of arrays belongs to the controller. Element-number exposure belongs to the worker.
- Existing patterns preserved:
- Option B expansion.
- The
preCheck*andReturnErrorpatterns. - Hook variables for test seams (
validateResreqSyntax). - Unity C tests in
testsuite/and table-driven Go tests.
- New components and why they exist:
parse_jobseland the two Gojobselmodules collapse ten lenient parsers into one grammar each.idgen_arrayexists because one-element arrays need a distinct request.autovars.cexists to make$jobid[...]substitution unit-testable.
- Steering compliance: TDD per component; errors are negative errno or
errnoxin C andReturnErrorplus zap in Go.
Technology Stack
| Layer | Choice / Version | Role in Feature | Notes |
|---|---|---|---|
| CLI | Go (xcecli module), Cobra | Identity parsing, submit output, help | No new dependencies |
| Backend services | Go (apiserver, gin) + cgo to libxce | Submit expression, REST selections, config | GenerateArrayJobID cgo wrapper plus non-cgo stub |
| Daemons | C gnu17 (controller, scheduler, worker), libxce | Validation, expansion guard, env | strtoull with errno replaces atoi |
| Test harness | C (xce_test) + Unity |
jobsub -a, $jobid, element commands |
New autovars.c; cmdutil.c extended |
| Data / Storage | WAL, snapshots | Unchanged | No format change (option B) |
File Structure Plan
New Files
src/lib/testsuite/test_jobsel.c # C conformance cases for parse_jobsel/parse_jobid (selection-cases table)
src/harness/autovars.c # autovar_substitute(): $jobid and $jobid[...] token substitution (moved out of test.c)
src/harness/include/autovars.h
src/harness/testsuite/test_autovars.c # 4.10, 4.11 unit tests
src/frontend/apiserver/controllers/jobsel.go # REST identity/name selection parser + key validator
src/frontend/apiserver/controllers/jobsel_test.go # selection-cases table + REST-only cases (5.17, decimal keys)
src/frontend/apiserver/controllers/jobarray.go # submit index-expression parser (2.1-2.9) for bracketed names; createArrayName keeps plain names
src/frontend/apiserver/controllers/jobarray_test.go
src/frontend/apiserver/controllers/jobs_handler_test.go # //go:build !cgo: handler-level submit/query/control tests through the test-seam variables
src/frontend/xcecli/common/format/jobsel.go # client identity selection parser + list splitter
src/frontend/xcecli/common/format/jobsel_test.go
src/frontend/xcecli/cmd/jobs/controller_test.go # controlJobsByIDs against an httptest server (request.NewAPIClient(srv.URL)): X[1-3] reaches the path as <decimal>[1-3]
src/controller/testsuite/test_jobsubmit_validate.c # 7.1-7.11 handle_jobsubmit validation/collision cases
src/worker/testsuite/test_exdata_jobindex.c # 6.1-6.4
Modified Files
- libxce
src/include/xce/errnox.h: addE_XCE_ARRAY_START0(beforeE_XCE_MAX) and its message text wherevererrnoxstrings are defined.src/include/utils.h,src/lib/utils.c:- Add
jobsel_t,jobsel_err_t,parse_jobsel(),jobsel_strerror()andis_array_tag(). - Re-implement
parse_jobid()overparse_jobsel()with the same signature: NULL-safe, bounded, ranges rejected. - Add
JOBID_FIELD_MAX_*constants.
- Add
src/include/idgen.h,src/lib/idgen.c:- Add
idgen_array(). idgen()accepts size 1 only.
- Add
src/lib/testsuite/test_idgen.c: array cases move toidgen_array(first element index 1, bounds 1..32767, block overflow refused).src/lib/meson.build(libxce_test_src) and the runnersrc/lib/testsuite/test.c: registertest_jobsel.
- Controller
src/controller/job.c:- Add
jobsubmit_classify()(pure) andarray_identity_exists(). - Registry entries gain
array_id, and the overlap test considers it. handle_jobsubmitorder: classify, then register, then table checks with release on a hit. Add the 7.1 log line.
- Add
src/controller/testsuite/test_commit_barrier_reply.c: array cases use taggedblock|1IDs.src/controller/testsuite/test_commit_barrier_rollout.c: arrays use at most 32767 elements with tagged first IDs.src/controller/meson.buildand the runnersrc/controller/testsuite/controller_test.c: registertest_jobsubmit_validate.src/controller/job.c(also):inflight_reply_and_remove();jobsubmit_async_ctx_releaseno longer removes registry entries; underXCE_TESTING,jobsubmit_test_reply_hook,jobsubmit_inflight_overlaps_for_test(),jobsubmit_inflight_overlaps_locked_for_test()andjobsubmit_inflight_trylock_for_test()(declared under#ifdef XCE_TESTING, as incluster.h:110-118).
- Scheduler
src/scheduler/src/driver.c:- Expansion guard (tag 1 and
size <= 32768 - idx, else log ERROR and skip the array). - The INFO log prints
array_jobid(first).
- Expansion guard (tag 1 and
src/scheduler/testsuite/test_driver_translate.c: tagged IDs, plus guard cases.src/scheduler/testsuite/test_job_array_expand.c(bases 5000, 1, 8000 at :182, :213, :283) andtest_job_array_wire_e2e.c(42000, 43000 at :295, :338-339): move to taggedblock|1bases, since the guard skips untagged arrays.
- Worker
src/worker/src/xcecd_exdata.c:XCE_JOBINDEXisarray_idx(jobid)for tag 1 and 0 otherwise.- A user
XCE_JOBINDEXis skipped with a WARN.
src/worker/testsuite/meson.build: register the new test.
- Harness
src/harness/cmdutil.c,src/harness/include/cmdutil.h: addcmdutil_parse_array_size(),cmdutil_job_arg()andcmdutil_job_not_found().src/harness/testsuite/test_cmdutil.c: cases for the above.src/harness/test.c:jobsub:-athrough the new parser;idgen_arraywith its return code checked;$jobidset to the array ID; help text.substitute_autovarsreplaced byautovar_substitute.expect-jobandjoblist_querymatch by number.- Every single-job command uses
cmdutil_job_argandcmdutil_job_not_found.
src/harness/meson.build: addautovars.ctoxce_test_srcand to a newtest_autovarsunit-test target.src/harness/testsuite/test_test_cli.c: pty cases (no controller needed) for thejobsub -arefusals and forjobkill -j 1-1-1-0[1-3](the 4.8 refusal, raised during option parsing before any RPC).
- Apiserver
src/frontend/apiserver/backend/clib.go,stub.go: addGenerateArrayJobID(cluster, apiserver, n).src/frontend/apiserver/backend/convert_pure.go: addParseHumanJobIDStrict(field widths),IsArrayCapableKey, andMakeElementID(key, k).src/frontend/apiserver/backend/handler.go: thedispatchJobArraylog names the array ID (FormatJobID(int64(ExtractArrayBaseID(baseJobID))); the parameterbaseJobIDholds the first element ID).src/frontend/apiserver/utils/types.go:QueryOptionsgainsPredicates []func(OrderMap) bool.src/frontend/apiserver/store/query.go:ExecuteQueryappliesPredicatesafterConditionsand before sort and pagination (fixes bug-1898 on the owned paths).src/frontend/apiserver/controllers/jobs.go:preCheckJobRequest: the interactive-name check runs first; then thejobarray.goparser.CreateJob: sizing and ID throughGenerateArrayJobID; the response; test-seam variablesgenerateJobID,generateArrayJobID,submitJob,controlJob(used at both control dispatch sites).GetJobsInfo,GetJobByIDs,ControlJobByID,FetchJobListand filter-basedControlJobroute throughjobsel.go.foundJobgains anexplicitIndexflag (full signature in ApiControl).- Not-found control result maps (
ControlJobByIDandControlJob) gainunique_job_id. - The most-recent-job rule (5.10).
createArrayNameis kept, called only for names without[or](1.4); deleteparseArrayIndexSectionand theIsNumericuses on these paths.
src/frontend/apiserver/models/jobs.go:JobSubmitResponsegainsArraySize int json:"array_size,omitempty". (Control results aremap[string]interface{}built injobs.go; there is no apiserver control-item struct.)src/frontend/apiserver/utils/config.go,config/app.yaml: clamp the limit to 32767 with a startup warning. The rule moves into a pureresolveMaxArraySize(v int) (effective int, clamped bool, err error);InitConfcalls it and emits theutils.Warn.controllers/jobs_test.go,utils/config_test.go,backend/convert_test.go,store/query_test.go: new cases.TestFoundJob_MatchesRealArrayIdxType(jobs_test.go:179) moves to the newfoundJobsignature and gets a taggedUnique_JobIDin its fixture; the fourpreCheckJobRequestcall sites (jobs_test.go:28, :100, :118, :217) become_, _, err := preCheckJobRequest(req).
- xcecli
src/frontend/xcecli/common/format/jobid.go:ParseJobIDbecomes bounded (delegating tojobsel.go).- Signature change:
ToAPIJobID(s string)(jobid.go:134) becomesToAPIJobID(s string, allowRange bool), implemented asParseSelection(s, allowRange)thenAPIKey(). Call sites updated:cmd/jobs/controller.go:52(controlJobsByIDs) passesallowRange=true;filters/job_filter.go:49andcommon/format/jobid_test.go:83, :96, :109passallowRange=false;peek.go:81moves toToAPIJobIDLegacy. unrecognised text is no longer passed through. Control (cmd/jobs/controller.gocontrolJobsByIDs) calls it withallowRange=true, sokill X[1-3]still reachesControlJobByID(5.2, 5.3, 5.5); queries (filters/job_filter.go) call it withallowRange=false. Plain decimal keys still pass through to the server's checks, after the client's own 5.7/5.8 checks when[...]is present. ToAPIJobIDLegacy(s)keeps today's passthrough behaviour forcmd/jobs/peek.go:81(peek is out of scope).FormatArrayChildIDkeeps its signature and is used only as a fallback.
src/frontend/xcecli/cmd/jobs/controller.go:ParseCmdArgsusesSplitSelections.controlJobsByIDsvalidates every selection before sending.- Results print
FormatJobID(UniqueJobID)when it is present. - Help text (9.2).
src/frontend/xcecli/filters/job_filter.go:SplitSelections,isValidJobIdreplaced byParseSelection, and the raw-Jvalue checked withParseNameSelectionbefore the wildcard mapping (:81-91); for a bracketed value the search rule and*stripping apply to the name part only.src/frontend/xcecli/cmd/jobs/controller.go(also): the raw control-Jvalue checked withParseNameSelectionbeforecontrolJobsByFilter.src/frontend/xcecli/cmd/jobs/submit.go:- Prints the array ID and element count.
- Help text (9.1).
src/frontend/xcecli/cmd/jobs/list.go: help text-Jcorrection.src/frontend/xcecli/models/jobs.go:JobSubmitDatagainsArraySize;JobControlResponseItemgainsUniqueJobID int64 json:"unique_job_id,omitempty".src/frontend/xcecli/cmd/jobs/peek.goandpeek_test.go(:18,23): switch toToAPIJobIDLegacy(no behaviour change).common/format/jobid_test.go: inTestToAPIJobIDPassthrough(:107-117),"123456"stays a passthrough case;"123456[3]"is misaligned (123456 & 0x7FFF != 0) and is replaced by the aligned"32768[3]"→"32768[3]", with a new rejection case asserting"123456[3]"returns the 5.8 error; malformed text is now rejected.
- Skills and docs
.claude/skills/xce-test-ha/run-ha-soak.sh,.claude/skills/xce-test-ha/SKILL.md: elements 1..N..claude/skills/xce-test-multihost/run-multihost-verify.sh,.claude/skills/xce-test-multihost/SKILL.md: element 1.src/docs/xce.md: new "Job arrays" section (9.4)..kiro/specs/job-array-single-wal-record/requirements.md(Requirement 5) and.kiro/specs/xce-test-e2e-primitives/requirements.md/design.md: superseded notes..kiro/specs/controller-job-cleanup/brief.md:86-87,.kiro/specs/job-dependency/brief.md:45-46: numbering updated..kiro/steering/roadmap.md: new entry.
System Flows
Controller array admission
sequenceDiagram
participant Client
participant Handler as handle jobsubmit
participant Registry as inflight registry
participant Table as jobmgr
participant Wal as commit barrier
Client->>Handler: jobsub first id and size
Handler->>Handler: classify
alt start zero
Handler-->>Client: E XCE ARRAY START0 and log
else invalid
Handler-->>Client: EINVAL
else ok
Handler->>Handler: field checks and hook
Handler->>Registry: try add range and array id
alt overlap
Registry-->>Handler: busy
Handler-->>Client: EEXIST
else added
Handler->>Table: element range exists or array id exists
alt collision
Handler->>Registry: reply EEXIST and release atomically
Registry-->>Client: EEXIST
else free
Handler->>Wal: commit array record
Wal-->>Handler: applied
Handler->>Registry: reply and release atomically
Registry-->>Client: first element id
end
end
end
-
classifyevaluates the checks in this order:- 7.1 (tag 1, index 0) →
INDEX0. - 7.4 (size 0), 7.5 (tag 2 or 3), 7.3 (tag 0 and size > 1), 7.2 (tag 1 and
size > 32768 - idx, written without addition so aUInt64size cannot wrap) →INVALID.
This gives 7.9:
INDEX0alone wins, and anyINVALIDwins over a collision, using the single error-EINVAL. - 7.1 (tag 1, index 0) →
-
Registration precedes the table checks, so any submission that commits is in the job table before it leaves the registry. Two concurrent submissions with the same array ID are ordered by who registers first (7.11).
-
An array submission is registered for its whole element range with
is_array = 1and itsarray_id(tag cleared, low 15 bits cleared). It is compared with every other array entry'sarray_id. Ordinary entries carryis_array = 0and take part in range overlap only.
Selection evaluation (all surfaces)
graph TB
Raw[raw argument] --> Syntax{grammar ok}
Syntax -- no --> E516[reject invalid selection]
Syntax -- yes --> Width{fields within width}
Width -- no --> E514[reject invalid identity]
Width -- yes --> Marked{decimal key with array marking and no index}
Marked -- yes --> E517[reject name elements as array id k]
Marked -- no --> Align{index given and key not array capable}
Align -- yes --> E58[reject invalid identity]
Align -- no --> Bound{number above 32767}
Bound -- yes --> E57[reject invalid selection]
Bound -- no --> Rev{range reversed}
Rev -- yes --> E55[reject range reversed]
Rev -- no --> Ok[parsed selection]
- The check order is exactly 5.12: 5.15/5.16, then 5.14, then 5.17, then 5.8, then 5.7, then 5.5.
- The harness parser has no decimal form, so the 5.17 node never applies there. Its syntax node is 5.15.
- For name selections (
-J,job_name), the width, marked and alignment nodes do not apply. The part before[is not checked against the submission array-name rule (2.2): a name no array can have simply selects no job (5.2).
Requirements Traceability
| Requirement | Summary | Components | Interfaces | Flows |
|---|---|---|---|---|
| 1.1 | name[1-N] creates 1..N |
ApiSubmitExpr, ApiCreateJob, IdgenArray, ControllerValidate | ParseSubmitIndexExpr, GenerateArrayJobID |
Admission |
| 1.2 | name[1] makes a one-element array |
ApiSubmitExpr, ApiCreateJob, IdgenArray | same | Admission |
| 1.3 | distinct element identities | IdgenArray, ControllerValidate | idgen_array, first + i |
Admission |
| 1.4 | names without brackets unchanged | ApiCreateJob (preCheckJobRequest) |
createArrayName path unchanged; isArray=false → GenerateJobID(..., 1) |
— |
| 1.5 | leading zeros | ApiSubmitExpr, ApiSel, CliSel, LibJobsel | number rule | Selection |
| 1.6 | configured limit used | ApiConfig | MaxArraySize |
— |
| 1.7 | clamp to 32767 with warning | ApiConfig | startup utils.Warn |
— |
| 1.8 | unset or 0 → 1000 | ApiConfig | — | — |
| 1.9 | negative refuses start | ApiConfig | InitConf error |
— |
| 1.10 | recovery keeps numbering | (unchanged WAL/apply; option B) | — | — |
| 2.1 | interactive arrays rejected | ApiSubmitExpr | interactive check before name branch | — |
| 2.2 | malformed expression or bad name | ApiSubmitExpr | ErrArrayName, ErrArrayMalformed |
— |
| 2.3 | lists or steps rejected | ApiSubmitExpr | ErrArraySingleRange |
— |
| 2.4 | %N rejected |
ApiSubmitExpr | ErrArrayRunLimit |
— |
| 2.5 | start 0 rejected | ApiSubmitExpr | ErrArrayStartZero |
— |
| 2.6 | reversed range | ApiSubmitExpr | ErrArrayReversed |
— |
| 2.7 | start > 1 rejected | ApiSubmitExpr | ErrArrayStartNotOne |
— |
| 2.8 | above limit | ApiSubmitExpr | ErrArrayTooLarge |
— |
| 2.9 | lowest-numbered rule reported | ApiSubmitExpr | ordered evaluation | — |
| 2.10 | nothing created on rejection | ApiCreateJob | early ReturnError before GenerateArrayJobID |
— |
| 3.1 | response names the array | ApiCreateJob, CliSubmit | JobSubmitResponse.jobid |
— |
| 3.2 | element count shown | ApiCreateJob, CliSubmit | array_size |
— |
| 3.3 | ordinary submit unchanged | ApiCreateJob | — | — |
| 4.1 | jobsub -a N makes 1..N |
HarnessJobsub, IdgenArray | cmdutil_parse_array_size, idgen_array |
Admission |
| 4.2 | -a 1 stays ordinary |
HarnessJobsub | idgen size 1 |
— |
| 4.3 | N > 32767 refused | HarnessJobsub | -ERANGE message |
— |
| 4.4 | bad, missing or 0 refused | HarnessJobsub | -EINVAL message |
— |
| 4.5 | success line and $jobid are the array ID |
HarnessJobsub | array_jobid |
— |
| 4.6 | ID failure reported | HarnessJobsub | idgen_array rc |
— |
| 4.7 | not-found hint | HarnessJobArg | cmdutil_job_not_found |
— |
| 4.8 | ranges refused | HarnessJobArg | cmdutil_job_arg |
Selection |
| 4.9 | exact ID acts as before | HarnessJobArg, HarnessExpectJob | numeric match | — |
| 4.10 | $jobid[...] substitution |
HarnessAutovars | autovar_substitute |
— |
| 4.11 | unset $jobid fails |
HarnessAutovars | same | — |
| 5.1 | <array-id>[k] display |
LibJobsel (display unchanged), CliControl, ApiControl | unique_job_id, fmt_jobid |
— |
| 5.2 | element selection acts on elements only | ApiSel, ApiMatch, CliSel | Selection, element-only filter |
Selection |
| 5.3 | missing numbers reported | ApiControl | per-number not-found items | — |
| 5.4 | missing [0] reported, no fallback |
ApiControl | foundJob(explicitIndex=true) |
— |
| 5.5 | reversed selection range | ApiSel, CliSel | ErrSelReversed |
Selection |
| 5.6 | bare ID acts on whole array and ordinary job | ApiMatch | bare form | — |
| 5.7 | number > 32767 rejected | ApiSel, CliSel, LibJobsel | *_EINDEX |
Selection |
| 5.8 | non-array-capable <array-id> rejected |
ApiSel, CliSel, LibJobsel | *_EALIGN |
Selection |
| 5.9 | array logs use the array ID | SchedGuard, ApiDispatchLog (dispatch + CreateJob log lines) |
log lines | — |
| 5.10 | most-recent rule for one-element array | ApiControl | isSingleElementArray |
— |
| 5.11 | query of missing [0] lists nothing |
ApiMatch | explicit-index filter | — |
| 5.12 | first failed check reported | ApiSel, CliSel, LibJobsel | ordered evaluation | Selection |
| 5.13 | harness [k] acts on that element |
HarnessJobArg | exact element ID | — |
| 5.14 | field-width rejection | ApiSel, CliSel, LibJobsel | *_EWIDTH |
Selection |
| 5.15 | harness syntax rejection | LibJobsel, HarnessJobArg | JOBSEL_ESYNTAX |
Selection |
| 5.16 | CLI/REST syntax rejection | ApiSel, CliSel, CliSplit | ErrSelSyntax, bracket-aware split |
Selection |
| 5.17 | marked decimal without [k] rejected |
ApiSel | ErrSelMarked |
Selection |
| 6.1 | XCE_JOBINDEX is the element number |
WorkerEnv | array_idx |
— |
| 6.2 | ordinary jobs get 0 | WorkerEnv | tag test | — |
| 6.3 | user value cannot override | WorkerEnv | skip in user loop | — |
| 6.4 | warning per host | WorkerEnv | XCECD_WORKER_LOG WARN |
— |
| 6.5 | XCE_JOBID unchanged |
WorkerEnv | — | — |
| 7.1 | start-0 array → distinct error and log | ControllerValidate | E_XCE_ARRAY_START0 |
Admission |
| 7.2 | last > 32767 rejected | ControllerValidate | INVALID |
Admission |
| 7.3 | untagged size > 1 rejected | ControllerValidate | INVALID |
Admission |
| 7.4 | size 0 rejected | ControllerValidate | INVALID |
Admission |
| 7.5 | other tags rejected | ControllerValidate | INVALID |
Admission |
| 7.6 | nothing stored; service continues | ControllerValidate | early reply, no registry | Admission |
| 7.7 | valid array created from its first number | ControllerValidate, ControllerCollision | classify OK, then commit | Admission |
| 7.8 | one-element array behaves as array | ControllerValidate, WorkerEnv, ApiMatch | tagged single path | — |
| 7.9 | error precedence | ControllerValidate | classify order | Admission |
| 7.10 | collisions → EEXIST |
ControllerCollision | array_identity_exists, registry array_id |
Admission |
| 7.11 | concurrent same array ID: first wins | ControllerCollision | registry before table | Admission |
| 8.1 | pre-upgrade element 0 kept | (unchanged storage), LibJobsel accepts [0] |
— | — |
| 8.2 | <array-id>[0] selects that element only |
ApiMatch, ApiControl, HarnessJobArg | explicit-index flag | Selection |
| 8.3 | element 0 shown as [0] |
CliControl, ApiControl | unique_job_id, FormatJobID |
— |
| 8.4 | recovery after upgrade | (unchanged WAL/apply) | — | — |
| 9.1 | submit help | CliSubmit | help text | — |
| 9.2 | control help | CliControl | help text | — |
| 9.3 | harness help | HarnessJobsub | usage strings | — |
| 9.4 | user documentation | Docs | src/docs/xce.md |
— |
Components and Interfaces
| Component | Domain/Layer | Intent | Req Coverage | Key Dependencies (P0/P1) | Contracts |
|---|---|---|---|---|---|
| LibJobsel | libxce | Bounded identity/selection parser | 1.5, 5.1, 5.7, 5.8, 5.12, 5.14, 5.15, 8.1 | config.h layout (P0) | Service |
| IdgenArray | libxce | Allocate an array's first element ID | 1.1-1.3, 4.1 | idgen ctx (P0) | Service |
| ControllerValidate | controller | Classify array submissions | 7.1-7.9 | LibJobsel is_array_tag (P0) |
Service |
| ControllerCollision | controller | Element and array-ID collision checks | 7.7, 7.10, 7.11 | jobmgr, registry (P0) | Service, State |
| SchedGuard | scheduler | Guard expansion; log the array ID | 5.9 | driver translation (P1) | Service |
| WorkerEnv | worker | XCE_JOBINDEX |
6.1-6.5, 7.8 | array_idx (P0) |
Service |
| HarnessJobsub | harness | jobsub -a and $jobid |
4.1-4.6, 9.3 | IdgenArray (P0) | Service |
| HarnessJobArg | harness | Single-job argument parsing and not-found reporting | 4.7-4.9, 5.13, 5.15, 8.2 | LibJobsel (P0) | Service |
| HarnessExpectJob | harness | Numeric expect-job |
4.9 | HarnessJobArg (P0) | Service |
| HarnessAutovars | harness | $jobid and $jobid[...] |
4.10, 4.11 | — | Service |
| ApiSubmitExpr | apiserver | Submit index expression | 1.1, 1.2, 1.5, 2.1-2.9 | ApiConfig (P0) | Service |
| ApiCreateJob | apiserver | IDs, submit, response | 1.1-1.4, 2.10, 3.1-3.3 | IdgenArray via cgo (P0) | API |
| ApiConfig | apiserver | Array-size limit | 1.6-1.9 | zap logger (P1) | State |
| ApiSel | apiserver | REST selection parser and key validator | 1.5, 5.2, 5.5, 5.7, 5.8, 5.12, 5.14, 5.16, 5.17 | backend ID helpers (P0) | Service |
| ApiMatch | apiserver | Selection → store filter | 5.2, 5.6, 5.11, 7.8, 8.2 | ApiSel (P0) | Service |
| ApiControl | apiserver | Control results, missing numbers, most-recent rule | 5.1, 5.3, 5.4, 5.10, 8.2, 8.3 | ApiMatch (P0) | API |
| ApiDispatchLog | apiserver | Array log line | 5.9 | — | — |
| CliSel | xcecli | Client selection parser | 1.5, 5.2, 5.5, 5.7, 5.8, 5.12, 5.14, 5.16 | — | Service |
| CliSplit | xcecli | Bracket-aware ID-list split | 5.16 | — | Service |
| CliSubmit | xcecli | Submit output and help | 3.1, 3.2, 9.1 | ApiCreateJob (P0) | — |
| CliControl | xcecli | Control by ID; result display; help | 5.1, 8.3, 9.2 | CliSel, ApiControl (P0) | — |
| Docs | docs | Syntax and LSF differences | 9.4 | — | — |
libxce
LibJobsel
| Field | Detail |
|---|---|
| Intent | Parse one written job identity, optionally with [k] or [a-b], under fixed bounds and check order |
| Requirements | 1.5, 5.1, 5.7, 5.8, 5.12, 5.14, 5.15, 8.1 |
Responsibilities & Constraints
- Grammar: optional leading/trailing spaces or tabs,
num-num-num-num, optionally[num]or[num-num].numis one or more decimal digits. There is no other whitespace and no sign. - Checks, in order: syntax (5.15), field widths (5.14: cluster <= 63, API server <= 15, timestamp < 2^32, sequence < 2^20), alignment when
[...]is present (5.8:seq % 32768 == 0), index bound (5.7: each number <= 32767), reversed range (JOBSEL_EREVERSED). The harness mapsJOBSEL_EREVERSEDto the 4.8 single-element refusal, because 5.5 applies only to the CLI and REST. - Overflow: digits are accumulated with an overflow guard (
strtoullwith errno, or a digit-count bound), so oversized numbers fail 5.14 or 5.7 rather than wrapping. - Display unchanged:
fmt_jobid,array_jobidandarray_idxkeep their behaviour.
Contracts: Service [x]
Service Interface
typedef enum {
JOBSEL_OK = 0,
JOBSEL_ESYNTAX, /* 5.15 */
JOBSEL_EWIDTH, /* 5.14 */
JOBSEL_EALIGN, /* 5.8 */
JOBSEL_EINDEX, /* 5.7 */
JOBSEL_EREVERSED /* 5.5 */
} jobsel_err_t;
typedef enum { JOBSEL_BARE = 0, JOBSEL_SINGLE, JOBSEL_RANGE } jobsel_form_t;
typedef struct {
uint64_t key; /* BARE: the exact (untagged) ID; else the array ID */
jobsel_form_t form;
uint32_t lo; /* SINGLE: lo == hi == k */
uint32_t hi;
} jobsel_t;
jobsel_err_t parse_jobsel(const char* text, jobsel_t* out);
const char* jobsel_strerror(jobsel_err_t err);
int is_array_tag(uint64_t jobid); /* TAG(jobid) == TAG_JOB_ARRAY */
uint64_t jobsel_element_id(const jobsel_t* s, uint32_t k); /* MAKE_JOBID(.., TAG_JOB_ARRAY, key|k) */
/* unchanged signature; now bounded, NULL-safe, and returns -EINVAL for a
* range, a rejected selection, or NULL; 1 on success */
int parse_jobid(uint64_t* jobid, char* buf);
- Preconditions:
out != NULL. A NULLtextreturnsJOBSEL_ESYNTAX. - Postconditions: on
JOBSEL_OK,keysatisfies the field widths. ForSINGLEandRANGE,keyhas tag 0 and its low 15 bits clear. - Invariants: fields are masked to their widths before composition.
MAKE_JOBIDitself is unchanged.
Implementation Notes
- Validation:
test_jobsel.cruns theselection-casestable (Testing Strategy). - Callers of
parse_jobid: the nine harness single-job commands (test.c:3327, 3517, 4140, 4276, 4921, 5073, 5268, 5377, 5481), which move tocmdutil_job_arg, andscheduler/src/repl_info.c:114. The REPLinfo jobargument is out of scope, and its behaviour does not change: on anyparse_jobidfailure it falls back tostrtoull(repl_info.c:114-117), which still yields the decimal value, 0, or the leading digits as today.
IdgenArray
| Field | Detail |
|---|---|
| Intent | Return the first element ID of a new array of n elements, numbered from 1 |
| Requirements | 1.1, 1.2, 1.3, 4.1 |
Contracts: Service [x]
/* 1 <= n <= MAX_ARRAY_IDX (32767). On success *first = MAKE_JOBID(c, a, ts,
* TAG_JOB_ARRAY, (block << 15) | 1) and returns n. -EINVAL for bad args or n;
* -EOVERFLOW if the next block would leave the 20-bit sequence (the context
* is left unchanged). Advances the context past the whole block. */
int idgen_array(idgen_ctx_t* ctx, size_t clusterId, size_t apiserverId,
size_t n, uint64_t* first);
/* Ordinary IDs only: arraySize must be 1 (else -EINVAL). Returns 1. */
int idgen(idgen_ctx_t* ctx, size_t clusterId, size_t apiserverId,
size_t arraySize, uint64_t* pidx);
- Postconditions:
first + n - 1has the same array ID and indexn. - Go wrapper:
GenerateArrayJobID(clusterID, apiserverID int, n int) (uint64, error)inbackend/clib.go, plus a matchingstub.gostub that returns an error. GenerateJobID: its only callers areCreateJobfor ordinary jobs andRestartJobByID, so it is always called with size 1; thearraySizeparameter stays for signature stability.idgencallers:clib.go:341(throughGenerateJobID) and the harnesstest.c:3721, which switches toidgen_arrayfor N >= 2.
Controller
ControllerValidate and ControllerCollision
| Field | Detail |
|---|---|
| Intent | Admit only representable array submissions; detect identity collisions without a window |
| Requirements | 7.1-7.11 |
Responsibilities & Constraints
- Pure classification:
jobsubmit_classifyruns first -- as the first statement afterinit_reply_by_reqinhandle_jobsubmit(there is no NULL check onsubmittoday, and the existing field checks dereference it the same way), before the existing field checks (NULLSTRuser/queue,start_info, emptycmdline,job.c:2445-2454), theHOOK_BEFORE_EVENThook and any registry or table access. It reads onlysubmit->jobidandsubmit->array_size, so this is safe, and it makes 7.9 hold for every submission: an index-0 array is answeredE_XCE_ARRAY_START0with its log line even when a field is also empty, and a 7.2-7.5 break is answered-EINVALbefore any field check. Consequence:HOOK_BEFORE_EVENTno longer fires for submissions rejected under 7.1-7.5 (today it fires for every submission that passes the field checks, including ones a later check rejects); such submissions create no job, so no hook consumer loses an event for a job that exists. - Registry and table: registry add first, then the table checks (7.10, 7.11). Answering a registered submission and removing its registry entry are one atomic step: a new
inflight_reply_and_remove(endpoint_t* ep, xce_peerinfo_t* from, xce_reply_t* reply, uint64_t base, uint64_t size)(the synchronous paths buildreplywithinit_reply_by_req; the async paths build it fromactx->seq/actx->opcodeand passactx->ep,actx->c->myhost, asjobsubmit_reply_*_oncedo today,job.c:2234-2288) takess_inflight_submits_lock, callsxce_reply(always non-blocking, so allowed under that lock byha-async-io.md), unlinks the entry, and unlocks. Every path after registration uses it -- the new table-hit path, the existing payload-build and allocation-failure early-outs (todayinflight_range_removethenxce_reply), commit failure or timeout and apply success (todayjobsubmit_reply_*_oncethenjobsubmit_async_ctx_release). It is the only place an entry is removed:jobsubmit_async_ctx_releasestops callinginflight_range_remove(job.c:2224) and only frees the async context, because a removal by exact(base, size)after the reply could delete a retry of the same submission that registered in between (7.7 allows that retry). On the async paths,jobsubmit_reply_*_oncecallsinflight_reply_and_removeinside its one-shot guard, so reply and removal happen together exactly once; a suppressed second reply removes nothing because the first already did. So another submission's collision check (which takes the same lock) sees an entry exactly while its submission is unanswered, as the Terms' "array submission still being accepted" requires: neither a just-answered submission blocking a later one (7.7) nor an unanswered one failing to block it (7.10, 7.11). - Array-ID check: for a tag-1 submission, one scan takes
lock(c->jobmgr, RTTI_LOCK_READ)once and looks up keysMAKE_JOBID(.., TAG_JOB_ARRAY, A|k)for k = 0..32767. That one scan answers both the element-range check (keys inside[first, first + size)) and the array-identity check (any key), replacingjobid_range_existsfor tag-1 submissions; tag-0 submissions keepjobid_range_existsunchanged. It is bounded CPU work (32768 hash lookups under one read-lock hold, no I/O, no blocking), so it is compatible with the vtask handler (ha-async-io.md, audit dimension 1). An array-ID index maintained at apply and snapshot load was considered and rejected: O(1), but it adds a derived structure that every apply, replay and snapshot-load seam must keep complete (dimension 7), for a cost paid only by array submissions. - No exits: no exit or abort path is added (dimension 2). Errors become reply statuses (dimension 5).
Contracts: Service [x] / State [x]
typedef enum { JOBSUBMIT_OK, JOBSUBMIT_INDEX0, JOBSUBMIT_INVALID } jobsubmit_class_t;
/* 7.1 first; then 7.4, 7.5, 7.3, 7.2; pure, no locks. size is the wire
* UInt64 (controller.capnp arraySize). 7.2 is evaluated without addition,
* as size > 32768 - idx (idx >= 1 here), so neither 2^32+2 nor UINT64_MAX
* can wrap past it. */
jobsubmit_class_t jobsubmit_classify(uint64_t first_jobid, uint64_t size);
/* One read-locked scan of the 32768 element keys of array_id's block.
* Returns 1 if any of them exists (array identity taken -- this covers the
* element range [first, first+size) too), 0 otherwise. */
int array_identity_exists(cluster_t* c, uint64_t array_id);
/* registry entry: { base_jobid, array_size, is_array, array_id }. Overlap if
* element ranges intersect, or both entries have is_array and equal array_id. */
-
Replies:
Class Reply status Log INDEX0-E_XCE_ARRAY_START0ERROR naming the job; says array element numbers start at 1 and the submitting client must be upgraded INVALID-EINVAL— Collision -EEXIST(unchanged)— Success reply jobid= first element ID (unchanged)— -
State: the registry keeps its mutex. Entries gain one field. Removal is by exact
(base, size), now only insideinflight_reply_and_remove. -
Invariant cross-check:
Dimension Outcome 1: async I/O Pass: bounded CPU only 2: lifecycle Pass 4: commit/apply Pass: unchanged commit path; registry ordering tightened 5: errors Pass: every rejection replies 7: field-complete apply Not affected: no WAL field change 9: redirect Not affected: no new primary-only operation
Scheduler
SchedGuard
- Expansion guard: for
XCE_SCHED_OP_UPDATE_JOB_ARRAY, if!is_array_tag(first)orsize > 32768 - array_idx(first)(no addition, so aUInt64size cannot wrap), log ERROR (namingarray_jobid(first)and the size) and expand nothing. The controller never sends such an array, so this guard is defensive only. - INFO log: prints
array_jobid(first)asarray_jobid=%s(5.9). - Scheduler invariants: expansion stays in
driver.ctranslation before core events, so the recording and the simulator see expanded jobs and driver state-equivalence (scheduler dimension 1) holds. Dimensions 2-4 are not touched.
Worker
WorkerEnv
- System value:
XCE_JOBINDEXisarray_idx(jobid)whenis_array_tag(jobid), and0otherwise (6.1, 6.2, 7.8). - User value: in the user-variable loop, a variable whose name is exactly
XCE_JOBINDEXis not applied, andXCECD_WORKER_LOGWARN names the job ID and the ignored variable (6.3, 6.4). XCE_JOBID: handling is unchanged (6.5).- Unchanged structure: the function's other caller (the JSON-parse failure path) is unchanged.
Harness
HarnessJobsub, HarnessJobArg, HarnessExpectJob, HarnessAutovars
/* digits only (leading zeros ok); -EINVAL missing/non-digit/0, -ERANGE > 32767 */
int cmdutil_parse_array_size(const char* text, uint32_t* n);
/* Trims spaces/tabs, parse_jobsel(). RANGE -> refuse (4.8). Writes a message
* into errbuf on rejection. On success *jobid is the exact ID (element ID for
* [k]), *had_index says whether [k] was given. Returns 0 or -EINVAL. */
int cmdutil_job_arg(const char* arg, uint64_t* jobid, int* had_index,
char* errbuf, size_t errlen);
/* Prints "job <arg> not found" and, when !had_index, "array elements must be
* named as <array-id>[k]" (4.7). */
void cmdutil_job_not_found(FILE* out, const char* arg, int had_index);
/* Values today's substitute_autovars() reads from test.c globals. */
typedef struct {
const char* jobid; /* NULL when unset */
int last_status;/* $? */
int jobcount; /* $jobcount, -1 when unset */
} autovar_ctx_t;
/* $?, $jobcount, $jobid and (new) $jobid[...] token substitution (4.10,
* 4.11); same token rule as today; '!' and '#' lines untouched. Returns 0,
* -EINVAL for an unset $jobid / $jobcount with today's messages, or -ENOSPC
* when out is too small (as today). The caller passes jobid = NULL when
* g_last_jobid is empty. */
int autovar_substitute(const char* line, const autovar_ctx_t* ctx, char* out,
size_t outlen, char* errbuf, size_t errlen);
jobsub -a N:- N = 1 →
idgen(ordinary). - N >= 2 →
idgen_array; a failure prints the error and submits nothing (4.6). - The ack check compares against the first element ID.
- Sequence seed:
test.c:6896-6899seedsidctx.sequencerandomly in[0, 2^20). Any seed above31 * 32768would make the firstidgen_arrayof that second return-EOVERFLOW(about 1 run in 32). The seed is bounded to[0, 30 * 32768), which leaves room for at least one whole block. (The apiserver's context starts at 0,clib.go:332.) - The success line and
$jobidusefmt_jobid(array_jobid(first))(4.5).
- N = 1 →
- Single-job commands: every command listed in the requirements' Harness boundary note calls
cmdutil_job_argand stops on its error. The controller's not-found replies go throughcmdutil_job_not_found. Forjobinfo, each-jis judged on its own. expect-job: parses its argument withcmdutil_job_arg.joblist_querymatchesjobs[i]->jobid == id. "Not found" and "refused" fail the assertion.
Apiserver
ApiSubmitExpr
type ArrayExprError struct {
Rule string // "2.1" ... "2.8"
Msg string // user-facing, quotes the name where 2.2 requires it
}
func (e *ArrayExprError) Error() string
// Called only for names containing '[' or ']' (names without them keep the
// existing createArrayName path, 1.4).
// Checks in requirement order 2.1 .. 2.8 and returns the first failure (2.9).
func ParseSubmitIndexExpr(name string, interactive bool, limit int) (
arrayName string, n int, isArray bool, err *ArrayExprError)
- Grammar: exactly 2.2's. Numbers are read digit-wise and saturate at a sentinel above any limit instead of overflowing. Rules 2.5-2.8 are then evaluated in order on the saturated values, so
name[99999999999999999999]reports 2.7,name[99999999999999999999-5]reports 2.6, andname[0-99999999999999999999]reports 2.5 (2.9). Leading zeros are accepted (1.5). - 2.3 / 2.4 / 2.5 / 2.6 / 2.7 / 2.8: 2.3 lists or steps; 2.4
%N; 2.5 first number 0; 2.6 last < first; 2.7 first > 1; 2.8 last > limit. - Contract with
CreateJob:preCheckJobRequestbecomespreCheckJobRequest(req) (n int, isArray bool, err error).CreateJobbranches onisArray, not on a count:isArray == true→GenerateArrayJobID(c, a, n)(n may be 1, soname[1]is a one-element array, 1.2, 7.8);isArray == false→GenerateJobID(..., 1)as today. Every existing error return inpreCheckJobRequest(jobs.go:1546,:1553,:1558,:1565,:1571,:1597,:1735, ...) becomes0, false, err; the plain-name success path (the named-job early return,:1578) and the unnamed-job success path (the final return,:1746) return1, false, nil. On success for a bracketed name,preCheckJobRequestsetsreq.JobName = arrayName(as today atjobs.go:1574, so the stored name isarr, notarr[1-3], and-J 'arr[2]'matches) and returns(n, true, nil)at the same point as today's named-job early return (jobs.go:1578), so the checks it skips are the same as today. The old sizing rule (arraySize := 1; if len(parsedIndices) > 1,jobs.go:1419-1423) is removed. - Names without
[or](1.4): keep today's path unchanged --createArrayNamestays and is called only for such names, soisValidArrayNameon the whole name, the slot-limit errors for a%suffix (jobs.go:1840-1851), the "job array list format is invalid: " prefix and their 500 status are exactly as before (they are not*ArrayExprError).ParseSubmitIndexExpris called only for names containing[or]. - Caller:
preCheckJobRequest(jobs.go:1537, no gin context) callsParseSubmitIndexExprfirst for a bracketed name, before the existing early return for named jobs, so the interactive check (2.1) is reached. It passesinteractive = (req.Interactive != 0)andlimit = utils.JobYamlConfig.MaxArraySize(the valueresolveMaxArraySizeproduced at startup; today read atjobs.go:1901) (xcecli sends 1, 2 or 3,submit.go:245-249). When the returned*ArrayExprErroris non-nil it returns0, false, aeunwrapped (no "job array list format is invalid: " prefix,jobs.go:1565, 1571); a nil*ArrayExprErroris never converted into a non-nilerror(typed-nil). - HTTP mapping:
CreateJob(jobs.go:1394-1398, todayReturnError(ctx, common.ERROR_CODE, ...)= 500 for every preCheck error) first checksvar ae *ArrayExprError; if errors.As(err, &ae) { ReturnError(ctx, common.INVALID_PARAMS_CODE, ae.Msg, nil); return }, then falls through to the existing 500 path for other errors.
ApiCreateJob
| Method | Endpoint | Request | Response | Errors |
|---|---|---|---|---|
| POST | /xce/v1/jobs | JobSubmitRequest (unchanged) |
JobSubmitResponse{jobid, queue_name, message, array_size} |
400 (2.x), 500 (ID allocation/submit) |
- Test seams:
controllers/jobs.goadds package variables in the existingvalidateResreqSyntaxhook pattern --var generateJobID = backend.GenerateJobID,var generateArrayJobID = backend.GenerateArrayJobID,var submitJob = services.SubmitJob,var controlJob = backend.ControlJob-- andCreateJoband both control dispatch sites (ControlJobByID,jobs.go:2611;ControlJob,:2847) call them, so non-CGO handler tests can replace them and observe dispatch. - Arrays:
GenerateArrayJobID(c, a, n)returns the first element.- The model has
Unique_JobID= first,JobID= the array ID,ArrayIdx= 1. primaryJobID(used byprocessJobRequestandpostJobRequestfor the submit-time%J/%Isubstitution) stays the first element ID, so a new array's file names get%I= 1 and%J=<array-id>[1], as 9.4 documents.- It submits with
SubmitJob(&job, n)(unchanged). - The response
jobidis the array ID's decimal,array_sizeis n, andmessageisJob <array-id-decimal> is submitted to queue <q> (n elements).
- The model has
- Ordinary jobs: unchanged, with
array_sizeomitted (3.3).
ApiConfig
job.max_array_sizeabsent or 0 → 1000 (1.8).- Negative → startup error (1.9).
- 1..32767 → used as is (1.6).
- Above 32767 → 32767, plus
utils.Warn("job.max_array_size above supported maximum", zap.Int("configured", v), zap.Int("effective", 32767))(1.7). The existing> 2147483646startup rejection (utils/config.go:221) is deleted; only a negative value is an error.
ApiSel and ApiMatch
type SelForm int
const ( SelBare SelForm = iota; SelSingle; SelRange )
type Selection struct {
Raw string // as the user wrote it (trimmed)
Name string // name selections only (job_name / -J)
Key uint64 // identity selections: exact ID (bare) or array ID
Form SelForm
Lo, Hi int // element numbers, Lo == Hi for SelSingle
}
type SelError struct {
Rule string // "5.16", "5.14", "5.17", "5.8", "5.7", "5.5"
Msg string // names the selection as written
}
// Identity: written c-a-ts-seq (strict widths) or decimal uint64 (ParseUint, base 10).
// Ranges are always allowed at the server.
// An identity range in a query is evaluated by its predicate (today it was
// accepted and silently dropped, bug-1898); only xcecli's own ParseSelection
// refuses query ranges, after 5.16, 5.14, 5.8 and 5.7 pass.
func ParseIdentitySelection(raw string) (Selection, *SelError)
// Name: 5.16 syntax, 5.7 and 5.5 only; no array-name rule check. The name
// part is zero or more characters, none of them whitespace, '[' or ']'. An
// empty name part is not a 5.16 violation: with contains/startwith/endwith it
// matches every name (today's semantics of an empty pattern); with eq/in/notin
// the name predicate requires a non-empty name, so it selects no job (eq/in)
// -- jobs submitted without a name (e.g. by the harness) are stored with
// job_name "" (backend/convert.go goString) and must not be matched by
// -J '[1]'.
func ParseNameSelection(raw string) (Selection, *SelError)
- Triggers: any value containing
[or]goes through these parsers before the existing plain-match fallback (5.16). Error messages pass through toReturnErrorunchanged, so they are no longer replaced by"bad job array index: "(5.5). - Callers:
GetJobsInfo(job_idandjob_name),GetJobByIDs,ControlJobByID,FetchJobList(and through it filter-basedControlJob). - Validate-all-first: each of these handlers parses and validates every selection and key in the request before any store query or control dispatch. In
GetJobsInfothis is a separate pass over all filter entries --ParseIdentitySelection/ParseNameSelectionon every value that needs it, plus the operator checks above -- run before the existing filter loop, which can query the store (exec_hostsresolution viaFetchJobIdList,jobs.go:465-480) and return early withcount: 0. Filter-basedControlJobvalidatesreq.Filter.JobNamewithParseNameSelectionitself -- only when it contains[or], since unnamed jobs carry their command (with spaces) as their name (jobs.go:1582-1584) andkill -J 'sleep 100'must keep working -- before callingFetchJobList, so a rejection returns 400 with the specific message (today everyFetchJobListerror is mapped to 500ERROR_QUERY_JOBINFO_MSG,jobs.go:2734-2738). The first rejection (in request order) is returned withReturnError, and the command acts on no job and reports no "not found" results (Terms). - Matching (ApiMatch): selections become row predicates passed in
QueryOptions.Predicates, evaluated bystore.ExecuteQuerybefore sorting and pagination (bug-1898). The bracketedjob_id/job_namevalues no longer produceCompFilterConditions.- Shape:
Predicates []func(OrderMap) bool, one composed predicate per filter field. Within one field (onejob_idorjob_namefilter), the values' predicates are OR'ed; across fields, and with the plainConditions, they are AND'ed, matching today's per-field filter semantics. - Whole-filter conversion: when any value of a filter entry has
[or], every value of that entry becomes a predicate (a bare value becomes aSelBarepredicate) and that entry, and only that entry, is removed fromConditions. Other entries on the same field with another operator (utils.ParseFiltersallows them,utils/utils.go:230-241) stay inConditionsand are AND'ed. Today's removal is keyed by field name and drops them too (jobs.go:494-512,:3420-3436). Sojob_id:in:(100,X[1])matches job 100 or elementX[1]. - Bare identity keys, one rule on both paths: every value of a
job_idfilter entry whose operator is an identity operator (eq,in,notin,ne), and every value of a converted bracketed entry, goes throughParseIdentitySelection. Values of other operators (betweenbounds,lt/lte/gt/gteoperands, string operators) are numbers or text, not identities, and stay on the plain path exactly as today when they carry no brackets. A written-form bare key (c-a-ts-seq) is normalised to its decimal value both on the plainjob_id in (...)path and inside a converted filter, so the same key matches the same job whether or not a bracketed value shares its filter (5.6). - Operators: the predicate honours the filter's operator instead of dropping it (bug-1898). It adds exactly one new rejection, for operator/value combinations that have no meaning with an element selection and were silently dropped until now: bracketed
job_idvalues with comparison or null operators and bracketedjob_namevalues with null operators (last bullets). They reuse today's "operator doesn't support" error:eqandin(both fields), andbetweenonjob_id: the row matches any of the values' selections. (job_id:betweenwith bracketed values had no defined meaning and was dropped; it is evaluated likein.job_name:betweenis not in the string table and stays rejected, below.)notin: the row matches none of them.job_namewithcontains,notcontains,startwithorendwith: the name part is evaluated with today's condition semantics -- built withutils.BuildFilterConditionsfor that operator and evaluated withstore.MatchesCondition(todaycontains/startwith/endwithall map to a case-insensitive substringOpLIKE,utils/types.go:143-148), so a bracketed and a plain value with the same operator match names identically -- and the row must also be an array element within the range (e.g. RESTjob_name:startwith:foo[1]selects element 1 of every array whose name matchesfoounder that operator). Fornotcontainsthe negation covers the name part only: the row is an array element within the range whose name does not contain the value. (notin, by contrast, negates whole selections, because its values are complete selections rather than one name pattern.)job_name:betweenstays rejected with today's "unsupported operator" error, as for plain values. xcecli's list path sends these operators for a leading and/or trailing*(generateSearchRule,filters/job_filter.go:191-205); for a bracketed-Jvalue it checks the raw text first (CliSelParseNameSelection, so*can only appear in the name part) and then takes the rule from, and strips*from, the name part only.ne: rejected with today's "operator doesn't support" error (jobs.go:254-261forjob_name,:357-364forjob_id), unchanged.- Any operator not in the field type's existing mapping table (
IntOperatorMappingTableforjob_id,StringOperatorMappingTableforjob_name) gets that table's existing "unsupported operator" error, as a plain value does today. job_idwithlt,lte,gt,gte,null,notnull(in the int table,utils/types.go:117-128), andjob_namewithnullornotnull(in the string table, but they ignore the value,utils/types.go:382-388), with a bracketed value: rejected with the existing "operator doesn't support" error -- the error classnealready gets today, not a new rejection category. These combinations had no meaning and were silently dropped (bug-1898); xcecli never sends them.- Control by name (
getFilters,jobs.go:2965-2970) always sendseqwith the raw value, sokill -J 'foo*[1]'matches the literal namefoo*. Wildcards are a list-only feature today, with or without brackets; that difference is pre-existing and out of scope.
SelBare→job_id == Key(all elements and any ordinary job; 5.6).SelSingle/SelRange→job_id == KeyandIsArrayJob(unique_job_id)andarray_idxin[Lo, Hi](5.2, 5.11, 8.2).- Name selections →
job_name == Nameandarray_idxin[Lo, Hi]withIsArrayJob(5.2). For the exact-match operators (eq,in,notin) an emptyNamematches no row, even though unnamed jobs are stored withjob_name == ""(backend/convert.go:28-31); sokill -J '[1]'(control always sendseq) acts on no job. - No special case for element 0:
<array-id>[0]/[0-0]get the identity predicate withLo = Hi = 0(8.2, 5.11);-J 'name[0]'and<array-id>[0-3](out of scope in the requirements) get the general name or range predicate, which matches array elements only.
- Shape:
- 5.17: a decimal key with tag 1 and no
[...]is rejected with "array elements must be named as<array-id>[k]". The written form never carries the tag. - Pre-store key checks:
GetJobByIDs(every key) and theGetJobsInfojob_idbranch (values of identity operators, and every value of a converted bracketed entry includingjob_id:between, see Matching), including thejob_id in (...)path, apply 5.14 and 5.17 to every such key, and 5.8 only to keys written with[...], before querying the store. A bare ordinary job ID is therefore never rejected for not being array-capable (5.6).
ApiControl
- Dispatch once (bug-1899):
ControlJobByIDtoday dispatches the accumulatedsuccessJobsinside its per-selection loop (jobs.go:2464,:2575-2617), so earlier selections are re-sent for every later one. After validate-all-first and matching, the selection loop collects (a) the jobs to act on, de-duplicated byunique_job_id, and (b) the not-found maps, one per missing number per selection, in selection order (not de-duplicated; 5.3, 5.4). It does not dispatch inside the loop. After the loop, for each job in (a), in order, it callsvalidateJobControl(job, action, signal)once (signature unchanged,jobs.go:3015; it checks one row) and appends its accepted and failed items; it then appends every not-found map from (b). It then dispatches each accepted job once throughcontrolJob; a dispatch error still returns 500 as today (jobs.go:2611-2616), it is not turned into a failed item. foundJob: becomesfoundJob(jobID int64, idx int, explicitIndex bool, rows []utils.OrderMap) ([]utils.OrderMap, error)(today:foundJob(int64, int, rows) ([]utils.OrderMap, error)). WhenexplicitIndexis true, only rows whoseunique_job_idis an array element (IsArrayJob) witharray_idx == idxmatch (5.4, 8.2). When it is false, today's whole-job_idmatch applies (5.6).- Not-found items: a missing number in a
SINGLEorRANGEselection yields one "job not found" map, whoseunique_job_idis the would-be element ID (5.3, 5.4). Accepted and failed result maps already carryunique_job_id(jobs.go:2575-2588,:2813-2826). - Result items: every result map therefore carries
unique_job_id= the element's own (or would-be) ID, so clients render<array-id>[k], including[0](5.1, 8.3). - Most-recent rule (5.10): applies only when the control filter contains no element number (
[...]) and the all-jobs option is not set. With an element number, the command acts on exactly the selected elements (5.2), whatever the newest row is. Without one, when the last matching row hasarray_idx == 0, or is the only stored element of its array, act on it alone. isSingleElementArray(row): counts the stored rows with thatjob_idthat are array elements. The API server's store records no array size, and the controller pushes a one-element array as a single job, so the stored row count is the only available signal. Deliberate approximation: a multi-element array whose other elements have all been purged from the store is treated as one-element. The multi-element rule is out of scope (requirements), so this only affects arrays already reduced to one stored element.
ApiDispatchLog
dispatchJobArrayreplaces itszap.Uint64("base_jobid", baseJobID)field (handler.go:273) withzap.String("array_jobid", FormatJobID(int64(ExtractArrayBaseID(baseJobID))))(5.9).CreateJob's two log entries (jobs.go:1434"Generated job ID" and:1515"Job submission successfully"), for an array, logarray_jobid(formatted) andarray_sizeinstead of the rawjobID; ordinary jobs log as today (5.9).
xcecli
CliSel and CliSplit
// Same grammar and order as ApiSel for identities (5.16, 5.14, 5.8, 5.7, 5.5).
// 5.8 and 5.7 apply to decimal keys with [...] too (tag 0, low 15 bits clear),
// so an upgraded client is protected against an un-upgraded server. Only 5.17
// (a marked decimal key without [k]) is left to the server.
func ParseSelection(raw string, allowRange bool) (Selection, error)
// Splits at commas outside [...] (as extractJobNames does).
func SplitSelections(list string) []string
// 5.16 / 5.7 / 5.5 for a raw -J value that contains '[' or ']', run before the
// wildcard mapping and '*' removal: the name part is zero or more characters,
// none whitespace, '[' or ']' ('*' allowed there only); after it exactly one
// [num] or [num-num] and nothing else. So -J 'foo[1]*' and -J 'foo[1*]' are
// rejected (5.16). A name part made only of '*' is accepted: -J '*[1]' is
// sent as job_name:contains:[1] (empty name part), i.e. element 1 of every
// array, as -J '*' already means every name today. For an accepted bracketed value the list path picks the
// search rule (generateSearchRule) from the name part alone and strips '*'
// from the name part only: -J 'foo*[1]' is sent as job_name:startwith:foo[1],
// -J '*foo*[1]' as job_name:contains:foo[1]. Values without brackets keep
// today's mapping. Used by list and control -J (control sends eq as today).
func ParseNameSelection(raw string) (Selection, error)
// "<decimal>" or "<decimal>[k]" or "<decimal>[a-b]" for the REST path.
func (s Selection) APIKey() string
// allowRange=false refuses a range ("a range of elements is accepted only by
// control commands") only after 5.16, 5.14, 5.8 and 5.7 have passed, so a
// query range breaking one of those reports that rule (5.12).
ToAPIJobID(s, allowRange): becomesParseSelection(s, allowRange).APIKey(). Control passestrue, queries passfalse. Text that is neither a written ID nor a decimal key is rejected rather than passed through.- Callers:
controlJobsByIDsandfilters/job_filter.goboth validate every selection before any request. The error message quotes the trimmed selection.
CliSubmit and CliControl
- Submit output: xcecli prints the server's
messageafter replacing<jobid-decimal>with<FormatJobID(jobid)>, as today. The count comes from the server message's(n elements)(3.2); the client does not append it again.array_sizeis mirrored in the client model for scripted use. - Control result lines:
FormatJobID(UniqueJobID)when it is non-zero, otherwise today'sFormatArrayChildID(compatibility with an older server). - Help (9.1):
name[1-N],name[1], numbering starts at 1, the limit is set by the API server (default 1000, at most 32767). - Help (9.2): kill, stop, resume and checkpoint show
<array-id>[k]and<array-id>[a-b].
Data Models
Domain Model
- Job ID (uint64) = tag (2) | cluster (6) | API server (4) | timestamp (32) | sequence (20).
- Array ID = tag 0, with the low 15 sequence bits 0.
- Element ID = tag 1 | array ID | k, with 1 <= k <= 32767 for new arrays and k = 0 only for arrays submitted before this change.
- Invariants:
- An array ID never equals an element ID.
- An array occupies element IDs
first .. first + n - 1within one 32768 block. - The controller accepts tag 0 (size 1) and tag 1 only.
Data Contracts & Integration
JobSubmitResponse(REST):jobid(int64, the array ID for arrays),queue_name,message,array_size(int, omitted for ordinary jobs).JobControlResponseItem(REST): addsunique_job_id(int64, omitempty).- Controller
OPCode_jobsub: wire format unchanged. New possible reply status-E_XCE_ARRAY_START0. - Persistence: no field is added to, or removed from, any WAL record, snapshot or recording. The save/restore completeness review does not apply: the per-field round trip is untouched, and 1.10 and 8.4 rely on the existing
JOB_ARRAY_SUBMITapply, which stores(first, size, submit)and expandsfirst + i.
Error Handling
Error Strategy
- C: negative errno or
-E_XCE_*reply statuses. Harness messages are written to stderr, and the command returns non-zero. Worker WARN goes throughXCECD_WORKER_LOG. - Go apiserver:
ReturnError(ctx, common.INVALID_PARAMS_CODE, msg, nil); returnfor 2.x, 5.x and 4xx-class rejections. ID allocation or submit failures usecommon.ERROR_CODEas today. Each rejection is logged atDebugwithzap.String("selection", raw). Startup clamping is logged atWarn. - xcecli: a client-side rejection exits non-zero before any HTTP request.
Error Categories and Responses
| Condition | Surface | Response |
|---|---|---|
| 2.1-2.8 | REST/CLI submit | 400 with the specific message |
| 5.5, 5.7, 5.8, 5.14, 5.16, 5.17 | REST/CLI query or control | 400; whole command rejected; no job acted on |
| 5.3, 5.4 | REST/CLI control | 200 with per-number "job not found" items |
| 7.1 | controller | -E_XCE_ARRAY_START0 + controller ERROR log |
| 7.2-7.5 | controller | -EINVAL |
| 7.10 | controller | -EEXIST |
| 4.3, 4.4, 4.6, 4.8, 4.11, 5.7, 5.8, 5.14, 5.15 | harness | stderr message; non-zero; nothing submitted or acted on |
| identity range in a query (client-side, as today) | xcecli list | non-zero exit before any HTTP request |
bracketed value with ne, a comparison or null operator, or an operator outside the field's mapping table |
REST/CLI query or control | 400 with the existing "operator doesn't support" / "unsupported operator" message; whole command rejected |
Monitoring
- Controller: one ERROR line per 7.1 rejection.
- Scheduler: an ERROR on a guarded expansion. Expected never; it would indicate a broken controller.
- API server: a startup WARN when the limit is clamped.
Testing Strategy
Shared conformance table selection-cases
One table of (input, expected outcome) rows is copied verbatim into test_jobsel.c, controllers/jobsel_test.go and common/format/jobsel_test.go. Each implementation runs the rows its surface supports.
| Group | Cases | Expected outcome |
|---|---|---|
| Valid | X, X[1], X[0], X[01], X[1-3], X[3-3], X[32767] |
accepted |
| Syntax (5.15/5.16) | X [1], X[a], X[], X[1, X], X[+1], X[-1], X[ 1], X[1-3:2], X[1]x, X[1-2-3] |
rejected |
| Widths (5.14) | cluster 64, API server 16, timestamp 2^32, sequence 2^20 (e.g. 0-0-T-1048576[1]) |
rejected |
| Alignment (5.8) | c-a-ts-5[3] |
rejected |
| Index (5.7) | X[32768], X[1-40000], X[99999999999999999999] |
rejected |
| Reversed (5.5) | X[3-1] |
rejected |
| Names (5.16; name surfaces only) | foo[1]; [1] (empty name: eq/in match no job, including unnamed jobs stored with job_name ""; contains matches every array); xcecli raw -J 'foo*[1]' (list sends job_name:startwith:foo[1]) and -J '*[1]' (list sends job_name:contains:[1]) |
accepted |
| Names (5.16) | foo [1], my job[1], foo[1]x; xcecli raw -J 'foo[1]*', -J 'foo[1*]' |
rejected |
| Precedence (5.12) | 0-0-T-1048576[40000] → 5.14, not 5.7; c-a-ts-5[3-1] → 5.8 |
first failing check reported |
Unit Tests
- libxce:
test_jobsel.cruns the table.parse_jobidNULL safety and range rejection.test_idgen.c:idgen_arrayfrom a seed in(31 * 32768, 2^20)returns-EOVERFLOWwith the context unchanged; a seed of exactly31 * 32768and seeds below30 * 32768succeed. Becauseidgenresets the sequence when the second changes (idgen.c:36-41), each seeded case setsctx->timestamp = time(NULL)immediately before the call and retries if the second changed in between;idgen_arrayreturns index 1; the last element has index n; n = 32767 is accepted and 0 or 32768 is rejected; block overflow returns-EOVERFLOWand leaves the context unchanged;idgenwith size 2 returns-EINVAL.
- Controller (
test_jobsubmit_validate.c):handle_jobsubmitwith an index-0 tagged array and an empty queue is answered-E_XCE_ARRAY_START0and writes the 7.1 ERROR line (classification precedes the field checks, 7.9).jobsubmit_classifyover all 7.1-7.5 combinations, including the 7.9 precedence, a size of 2^32+2 andidx = 2, size = UINT64_MAX(both rejected under 7.2, neither truncated nor wrapped).handle_jobsubmitwith an existing array ID (an existing elementX|3blocks a newX|5), giving 7.10.- Two registered concurrent submissions with the same array ID; the second gets
-EEXIST(7.11). - Reply and release are atomic. Seam (project convention for test-only symbols,
testing.md): under#ifdef XCE_TESTING,job.cexposesvoid (*jobsubmit_test_reply_hook)(void)-- called insideinflight_reply_and_removeafterxce_replyand before the unlink, with the registry lock held -- plusint jobsubmit_inflight_overlaps_for_test(uint64_t base, uint64_t size, int is_array, uint64_t array_id)(takes the lock),jobsubmit_inflight_overlaps_locked_for_test(...)(same check, caller already holds the lock) andint jobsubmit_inflight_trylock_for_test(void)(returnspthread_mutex_trylock's result, unlocking on success); declared under#ifdef XCE_TESTINGascluster.h:110-118does for other test-only symbols. (There is noxce_replytest hook insrc/lib,test_commit_barrier_reply.c:27, and replies are read fromep->outqonly after the handler returns, so neither order is observable without this seam.)- Deterministic checks inside the hook (no second thread, since a thread would just block on the lock and pass even against a split implementation): (1)
jobsubmit_inflight_trylock_for_test()returnsEBUSY(the registry lock is held across the reply); (2) A's reply is already queued on the endpoint'soutq; (3)jobsubmit_inflight_overlaps_locked_for_test()(lock-free variant) still finds A's entry. Afterinflight_reply_and_removereturns,jobsubmit_inflight_overlaps_for_test()finds no entry. - A retry of the same
(base, size)registered after A's reply is not removed by A's async-context release (jobsubmit_async_ctx_releaseno longer touches the registry). - Once A has been answered with a failure, a same-array-ID submission B is accepted (7.7); while A is registered and unanswered, B gets
-EEXIST(7.11).
- Deterministic checks inside the hook (no second thread, since a thread would just block on the lock and pass even against a split implementation): (1)
- A size-0 submission is answered
-EINVAL(7.4).
- Apiserver:
jobs_handler_test.go(new,//go:build !cgo): every handler-level case (CreateJob,GetJobsInfo,GetJobByIDs,ControlJobByID, filter-basedControlJob). It is!cgobecause the JWT/RBAC fixturessignTestJWT/setupRBACTestlive in the!cgo-onlycontrollers/integration_test.go:54, :83. It replaces the seamsgenerateJobID,generateArrayJobID,submitJobandcontrolJob(the last records each dispatchedunique_job_id, so "acts on" is observable), seedsstore.Jobs(with a controllablejob_status, sincevalidateJobControlchecks it,jobs.go:3045) andstore.Queuesfor every seeded job's queue (the role's allowed queues come fromstore.GetAllQueueNames(), otherwise control returns 403 before dispatch; patternrbac_convention_test.go:86), and setsutils.JobYamlConfig = &utils.JobConfig{MaxArraySize: 1000}, restoring every replaced global and store int.Cleanup. Order matters:setupRBACTestcallsresetIntegrationState()→store.InitStores()(integration_test.go:33-36, :83-86), which replacesstore.Jobsandstore.Queueswith empty stores; so each test saves the original store pointers, callssetupRBACTest, then seeds, and restores the saved pointers int.Cleanup. Every control test also asserts at least one expected dispatchedunique_job_id(or, for the act-on-no-job cases, that the fixture rows are present and nothing was dispatched), so an empty store cannot make it pass.- Submit:
arr[1-3]leavesreq.JobName == "arr"; one 2.x row returns 400 with the exact message; a valid ordinary submit is not rejected (typed-nil guard);name[1]goes throughgenerateArrayJobIDwith n = 1 and the response hasarray_size: 1; plain namesmy job,foo%5andfoo%get today's errors and 500 status; submit-time%I= 1 and%J=<array-id>[1]for a new array (9.4). - Query:
GetJobsInfo?filter=job_id:eq:X[2]returns only element 2 (bug-1898);GET /jobs/X[1-3]returns elements 1-3; element-only matching for<ordinary-id>[0](5.11); with an unnamed (job_name "") array element in the store,job_name:eq:[1]lists nothing whilejob_name:contains:[1]lists it; anexec_hostsfilter matching no host together withjob_id:eq:X[40000]returns 400 (5.7), notcount: 0;job_name:notnull:foo[1]gets the existing "operator doesn't support" error;job_name:notcontains:foo[1]selects element 1 of arrays whose name does not containfoo;{job_id:in:(X[1])},{job_id:ne:100}keeps theneentry;job_id:in:(X[1]);job_name:eq:foo[2]is an intersection;job_id:in:(100,X[1])matches both;job_id:notin:(X[1])excludes elementX[1];job_name:startwith:foo[1]selects element 1 of arrays namedfoo...;job_id:ne:X[1]keeps today's rejection; the same written-form bare key matches alone and mixed with a bracketed value. - Control: two selections in one request (
X[1],Y[2]) dispatch each element exactly once and list each once (bug-1899);X[1-3]dispatches to the existing elements and reports missing numbers as not-found items carryingunique_job_id(5.3);X[0]with no element 0 dispatches nothing and reports it not found (5.4); a pre-upgrade element 0 is dispatched alone (8.2); a request with a valid selection followed by an invalid one dispatches nothing (Terms); filter-based-J 'foo[40000]'returns 400 with the 5.7 message; filter-based-J '[1]'with an unnamed array in the store dispatches nothing; the most-recent rule with a one-element array dispatches that element alone (5.10);-J 'foo[1]'with two one-element arrays namedfoodispatches both (5.2).
- Submit:
jobs_test.go(untagged, pure functions only):foundJobwithexplicitIndex([0], 5.4, 8.2); the predicate builder for each operator rule in ApiMatch, evaluated overstore.Jobs-shaped rows;isSingleElementArray.
- xcecli:
jobsel_test.go: the table plusSplitSelections("X[1,3],Y").jobid_test.go: passthrough cases"123456"and"32768[3]";"123456[3]"rejected under 5.8;ToAPIJobID("32768[1-3]", true)passes through whileToAPIJobID("32768[1-3]", false)is refused.cmd/jobs/controller_test.go(new):controlJobsByIDswithrequest.NewAPIClient(srv.URL)pointed at anhttptestserver;X[1-3]reaches the request path as<decimal>[1-3](guardsallowRange=trueatcontroller.go:52).
- Harness:
test_cmdutil.c:cmdutil_parse_array_size(1,01,0,-3,+5,5,5.0,0x5,abc,32767,32768) andcmdutil_job_arg(ranges refused includingX[3-1]with the 4.8 message,[0]accepted).test_autovars.c:$jobid[3],$jobid[3]x, unset$jobid,$?,$jobcount(set and unset), and!/#lines.
- Worker (
test_exdata_jobindex.c): element index, ordinary 0, a userXCE_JOBINDEXignored with a WARN,XCE_JOBIDunchanged. - Scheduler (
test_driver_translate.c): taggedblock|1arrays expand to indices 1..n; untagged arrays, overflowing arrays andidx = 2, size = UINT64_MAXare not expanded.
Integration Tests
- Controller suite: existing commit-barrier tests pass with tagged first IDs (1.10 and 8.4 rely on the existing WAL replay tests, which still pass).
- Apiserver: CGO
--suite apiserversubmit throughGenerateArrayJobID(real libxce), asserting the responsejobidequalsarray_jobid(first)andarray_size. - Harness pty (
test_test_cli.c):jobsub -a 0andjobsub -a 40000are refused;jobkill -j 1-1-1-0[1-3]is refused with the 4.8 message before any RPC.
E2E Tests
/xce-test-multihost:jobsub -a 2elements[1]and[2]reach DONE on both hosts./xce-test-ha: a 6-round soak verifies elements 1..N survive failover (1.10).- Manual REST flow (recorded in validation):
xcecli jobs submit -J 'arr[1-3]'lists[1]..[3].xcecli jobs kill X[2]acts on element 2 only.- A job script prints
XCE_JOBINDEX.
Migration Strategy
- Recommended order for clusters that use arrays:
- Upgrade the API servers and the harness. An un-upgraded controller accepts their
array-id | 1submissions unchanged (it does not validate tags), and those arrays are numbered from 1 (Adjacent expectations). - Upgrade the controllers.
- Upgrade the workers, schedulers and xcecli in any order. The scheduler's expansion guard accepts arrays from un-upgraded clients (tagged, starting at index 0).
- Upgrade the API servers and the harness. An un-upgraded controller accepts their
- Controllers first is also supported: until the API servers and harness are upgraded, their multi-element array submissions are rejected with
E_XCE_ARRAY_START0, and the controller log explains why (7.1). Ordinary jobs are unaffected. Operators choosing this order accept that rejection window. - No data migration: pre-upgrade arrays keep element 0 (8.1-8.4).