Repository navigation
fix(aws-batch): job lifecycle, scheduling policies, job definition type check and job state-change events - #1648
Conversation
NitinKumar004
left a comment
There was a problem hiding this comment.
This lands the whole Batch job surface: SubmitJob/DescribeJobs/ListJobs/CancelJob/TerminateJob, scheduling-policy CRUD with queue binding, the job-definition type enum, and "Batch Job State Change" events. The design is right. Job state is derived at read time from SubmittedAt + Phase on the provider clock, with no goroutine per job. That is the same model as internal/settle. Events go out through the shared config.Tickable scheduler and are emitted outside jobsMu. Cancel and terminate are decided against the observed state at call time. The ListJobs cursor is keyed on the submission sequence, so it stays stable. Snapshots carry jobs, policies and the job sequence. The wire layer only translates. I found no blocking correctness bug. The points below are fidelity gaps, and one of them is a silent drop worth fixing in this PR.
Verified
go build ./...ok.go veton the touched packages ok.gofmtclean.go test -race -count=1passes forproviders/aws/batch,server/aws/batchandinternal/settle.providers/aws(Tickables) andpersistalso pass.golangci-lint --new-from-rev=baseon the touched packages: 0 issues.go generateleaves no diff.- I ran a concurrent Tick + Restore + submit probe under
-race -count=3. No race. - Terraform (hashicorp/aws 6.66.0, real provider, default endpoint override,
serve --async-settle):aws_batch_compute_environment(Fargate),aws_batch_scheduling_policy(fair share), twoaws_batch_job_queue(fair-share and FIFO), andaws_batch_job_definition(container and multinode) all applied.plan -detailed-exitcodereturned 0 for everything except the pre-existing multinode drift in point 5 below.- I then updated
weight_factorand the queuepriority. The apply succeeded and plan returned 0 again. destroysucceeded and follow-up describes return empty.
- AWS CLI:
- submit → describe shows STARTING → SUCCEEDED with
Essential container in task exited,exitCode 0, and retryStrategy/timeout echoed. dependsOn: the child stays PENDING while the parent is RUNNING.- Array job (
size=3): the parent is PENDING withstatusSummary.:1resolves and:3is omitted. - SEQUENTIAL array, then cancel on the parent:
:0SUCCEEDED,:1and:2FAILED, and the parent is FAILED withisCancelled. cancel-jobon a SUBMITTED job gives FAILED withisCancelled.- These all return ClientException with the right message: fair-share queue without
shareIdentifier,jd:99,size=10001, unknown job id,jobStatus DONE, a non-numeric BEFORE_CREATED_AT, deleting an in-use policy, removing a queue's policy,--type batch, and anaws:tag key. JOB_NAMEHEL*matches case-insensitively.
- submit → describe shows STARTING → SUCCEEDED with
- EventBridge rule to SQS: RUNNABLE, STARTING, RUNNING, SUCCEEDED, and no event for the submission. Source is
aws.batch, resources are[jobArn], and events for unpolled jobs arrive through the ticker. - No AI attribution in the PR body or in any of the 3 commits.
Medium
nodeOverrides,ecsPropertiesOverrideandeksPropertiesOverrideare accepted and silently dropped (providers/aws/batch/jobs.go:299).- The wire layer and the driver carry all three into
SubmitJobInput, butbuildJobonly mergescontainerOverrides. It copies the job definition's node/ECS/EKS properties unchanged. - Probe: I submitted with
ecsPropertiesOverrideand DescribeJobs returned the definition'secsProperties(null here) without the override. - A user who overrides the command of an ECS or EKS job, or the node ranges of an MNP job, sees the original definition echoed back. Nothing errors, and nothing says the override was ignored.
- Fix: merge them the way
containerOverridesis merged, or reject them / list the gap indocs/coverage/nongoals/batch.md. Today the drop isn't documented anywhere.
- The wire layer and the driver carry all three into
Low
- SubmitJob accepts an explicit INACTIVE revision (
jobs.go:267).- The
name:revision/ ARN-with-revision path returns the stored definition without checkingStatus. Probe: afterDeregisterJobDefinition jd:1, SubmitJob withjd:1succeeded (CLI too). - The bare-name path does require ACTIVE. I'd expect real Batch to refuse a deregistered revision with ClientException. Please confirm against the API reference and match it.
- The
- Replayed intermediate events carry the SUCCEEDED-era
container(events.go:192).eventDetailsetsContainer: j.Containerfrom the job as rendered now. Without--async-settle(the serve default) the job is already SUCCEEDED, so the RUNNABLE, STARTING and RUNNING events all carrycontainer.exitCode: 0. I confirmed this with a probe.statusReason,startedAtandstoppedAtare already stripped for replayed statuses.exitCodeshould be stripped the same way.
- A 1s phase makes TerminateJob on a RUNNING job hard to reach from a real client (
internal/settle/settle.go:85).- With
--async-settlea job goes from STARTING to SUCCEEDED in 2s. - Under load I polled until STARTING and then called
terminate-job. The job was already SUCCEEDED when the call landed, because CLI round-trips take more than a second. The same happened in a dependency test. - The provider test covers the semantics, but CLI or Terraform users can't reliably exercise terminate-vs-cancel. Consider a longer RUNNING dwell, for example 5 to 10s, or making the dwell configurable.
- With
- Pre-existing, not introduced here: multinode job definitions drift in Terraform.
RegisterJobDefinitiondefaultsplatformCapabilitiesto["EC2"]when none is sent.aws_batch_job_definitionwithtype = "multinode"and noplatform_capabilitiesthen plans- "EC2"forever (exit 2).- This PR rewrites the validation in the same function, so it could fix it here or in a follow-up: only default when real AWS does, or don't echo a default the caller didn't send.
- ListJobs
--array-job-idignoresfilters(jobs.go:641).children()only applies the status check. JOB_NAME and the other filters are validated, but the child list is returned unfiltered.--filters name=JOB_NAME,values=nomatchwould still list every child (this comes from reading the code; my live call used a matching name). Either applymatchesFiltersthere or reject filters with arrayJobId. - Check-then-act on scheduling-policy binding.
UpdateJobQueuereads the queue (job_queue.go:106) and then appliespolicyARNinside a separateUpdate.DeleteSchedulingPolicyscans the queues (scheduling_policy.go:258) and then deletes the policy, whileCreateJobQueueresolves the policy on its own.- A concurrent delete plus create or update can leave a queue pointing at a deleted policy. It needs concurrent control-plane calls, so this is low priority, but one small mutex around policy binding would close it.
Verdict: COMMENT, close to approve. Fix point 1, either in code or as a documented non-goal. Points 2 to 7 can follow.
| // buildJob assembles the static detail of a new job from the request, the | ||
| // queue and the job definition (submit-time values override the definition's). | ||
| func (m *Mock) buildJob(in *driver.SubmitJobInput, q *driver.JobQueue, jd *driver.JobDefinition) (driver.Job, error) { | ||
| container, err := mergeContainer(jd.ContainerProperties, in.ContainerOverrides) |
There was a problem hiding this comment.
Only containerOverrides is merged here. NodeOverrides, EcsPropertiesOverride and EksPropertiesOverride come in through the wire and the driver input but are never read, so DescribeJobs echoes the unmodified definition (probe: ecsPropertiesOverride set, ecsProperties came back null). Please merge them the same way, reject them, or at least list the gap in nongoals/batch.md. Right now they are dropped silently.
There was a problem hiding this comment.
Fixed in 689b550. buildJob now runs applyPropertyOverrides (new providers/aws/batch/overrides.go):
ecsPropertiesOverride:taskProperties[i]maps onto the definition'staskProperties[i], and each container override targets a container byname(an unnamed override is accepted only when there is exactly one container).environmentmerges by name,resourceRequirementsby type, andcommandreplaces.eksPropertiesOverride:containersandinitContainersby name (envmerges by name;image,command,argsandresourcesreplace), andmetadatalabels/annotations merge key by key.nodeOverrides:numNodesreplaces the count under the documented conditions (an open-ended range whose lower bound is below it, andmainNodebelow it), and eachnodePropertyOverridemerges into every node range itstargetNodescovers (containerOverrides, ECS/EKS overrides,instanceTypes, consumable resources).- An override the definition can't take is a ClientException rather than a silent drop: ECS/EKS override without matching properties,
nodeOverrideson a non-multinode definition, an unknown container name, or atargetNodescovering no range.
Tests: TestSubmitJobAppliesEcsPropertiesOverride, TestSubmitJobAppliesEksPropertiesOverride, TestSubmitJobAppliesNodeOverrides, TestSubmitJobRejectsUnusableOverrides, and SDK TestSDKSubmitJobEcsPropertiesOverride. The remaining gap (no per-node nodeDetails, no platform check such as nodeOverrides on Fargate) is now in nongoals/batch.md.
| func (m *Mock) resolveJobDefinition(ref string) (driver.JobDefinition, error) { | ||
| key := nameFromARN(kindJobDefinition, ref) | ||
|
|
||
| if strings.Contains(key, ":") { |
There was a problem hiding this comment.
The explicit-revision path skips the ACTIVE check that the bare-name path applies. After DeregisterJobDefinition jd:1, SubmitJob --job-definition jd:1 still succeeds (I reproduced it with the CLI). I'd expect Batch to return a ClientException for an INACTIVE revision. Can you check the API reference and add a jd.Status != JDStatusActive guard if it matches?
There was a problem hiding this comment.
Fixed in 689b550. The explicit-revision path (name:revision or an ARN with a revision) now requires Status == ACTIVE and returns a ClientException otherwise. The SubmitJob reference says the revision forms resolve a specific revision and a bare name uses the latest ACTIVE one, and an INACTIVE revision can't be submitted to (this is the known Step Functions failure mode after CloudFormation deregisters old revisions). Test: TestSubmitJobRejectsInactiveRevision (both jd:1 and the full ARN are rejected after deregistering, and jd:2 still works). Also confirmed with the CLI.
| JobArn: j.JobARN, JobName: j.JobName, JobID: j.JobID, JobQueue: j.JobQueue, | ||
| Status: status, Attempts: []any{}, CreatedAt: j.CreatedAt, | ||
| RetryStrategy: j.RetryStrategy, DependsOn: make([]jobEventDependency, 0, len(j.DependsOn)), | ||
| JobDefinition: j.JobDefinition, Parameters: j.Parameters, Container: j.Container, |
There was a problem hiding this comment.
Container comes from the job as rendered now. In sync mode (the serve default) that render is already SUCCEEDED, so the replayed RUNNABLE, STARTING and RUNNING events all carry container.exitCode: 0 (confirmed with a probe). Since status != j.Status already strips the reason and times, consider stripping exitCode from the container there as well.
There was a problem hiding this comment.
Fixed in 689b550. The replay branch now strips exitCode from the container (withoutExitCode) along with the reason and times, so only the terminal event carries it. Test: TestReplayedEventsCarryNoExitCode (sync mode: RUNNABLE/STARTING/RUNNING have no exitCode, SUCCEEDED has 0). Confirmed through an EventBridge rule to SQS on plain serve.
| // DefaultBatchJobPhase is how long an AWS Batch job dwells in each | ||
| // non-terminal phase (SUBMITTED, RUNNABLE, STARTING, RUNNING) before moving | ||
| // on, so a dependency-free job reaches SUCCEEDED 4 phases after submission. | ||
| DefaultBatchJobPhase = 1 * time.Second |
There was a problem hiding this comment.
With 1s phases, STARTING to SUCCEEDED takes 2s. Under normal CLI latency I couldn't land terminate-job while the job was STARTING or RUNNING: the job was already SUCCEEDED each time. A longer RUNNING dwell (5 to 10s) would let real CLI and SDK users exercise terminate vs cancel and dependency PENDING.
There was a problem hiding this comment.
Fixed in 689b550. Added settle.DefaultBatchJobRunDwell = 5s (additive; the other phases stay at 1s), stored per job as RunPhase so it survives snapshots. A record from an older snapshot without it falls back to the phase length. With the CLI I polled to RUNNING and terminate-job landed every time: FAILED, isTerminated: true, startedAt kept. Tests: TestJobLifecycleAdvancesWithClock (asserts stoppedAt - startedAt = 5s) and TestJobsAndPoliciesSurviveSnapshot (the dwell survives restore). I didn't make it configurable to keep the scope small; happy to add a flag later if it is wanted.
| return out | ||
| } | ||
|
|
||
| for i := int(l.after + 1); i < rec.arraySize(); i++ { |
There was a problem hiding this comment.
The array-child listing applies only the status filter. filters are validated but never applied here, so --array-job-id X --filters name=JOB_NAME,values=foo returns every child. Either run matchesFilters on each child or reject filters together with arrayJobId.
There was a problem hiding this comment.
Fixed in 689b550. children() now runs matchesFilters on each child, the same as the queue listing. Test: TestListArrayChildrenAppliesFilters (JOB_NAME=nomatch returns 0 children, ARR* returns all 3). The CLI gives the same result.
| } | ||
| } | ||
|
|
||
| cur, ok := m.jobQueues.Get(name) |
There was a problem hiding this comment.
This is a separate read before the Update closure, and DeleteSchedulingPolicy scans queues outside any lock as well. A concurrent delete plus UpdateJobQueue/CreateJobQueue can leave a queue bound to a policy that no longer exists. It's minor, but resolving the policy inside the closure, plus a small mutex shared with the policy delete, would close the window.
There was a problem hiding this comment.
Fixed in 689b550. Added policyBindMu. CreateJobQueue holds it across the policy resolution and the insert, UpdateJobQueue across the read, the resolution and the update, and DeleteSchedulingPolicy across the in-use scan and the delete. It is a leaf lock (no other mutex is taken under it), so it can't deadlock with jobsMu or registerMu. Test: TestSchedulingPolicyBindingIsAtomic races create/update/delete 50 times and checks that no queue references a deleted policy. It failed every run before the change and passes under -race -count=5 now.
|
Thanks for the thorough review, @NitinKumar004. All seven points are addressed in 689b550, including the pre-existing multinode drift, since the fix was small and safe.
Each new test fails on the previous head and passes now. Notes on the choices I made:
Verification:
Follow-ups (not in this PR):
Both of the first two are listed in |
NitinKumar004
left a comment
There was a problem hiding this comment.
Re-review of 689b550 against my review of 57fdca5. Every point from that review is fixed, and the new override code holds up against the CLI and Terraform. I have two small follow-ups on the override merge and one lint note. None of them blocks this PR.
Earlier findings
- (Medium) nodeOverrides, ecsPropertiesOverride and eksPropertiesOverride were dropped silently. Fixed.
applyPropertyOverridesinoverrides.gois now called frombuildJob. With the CLI:- An ECS override on
mainreplacedcommandand mergedenvironmentby name (X=1 kept, Y=2 added). Thesidecontainer was left alone. - An EKS override replaced
command, setenvand merged the metadata labels key by key. nodeOverridessetnumNodes2 to 4 and mergedcontainerOverridesinto the0:range.- These all return ClientException: an ECS override on a containerProperties definition, an unknown container name, an unnamed override with two containers, nodeOverrides on a container definition,
numNodes: 0, andtargetNodes "5:7". - The remaining gaps are listed in
nongoals/batch.md.
- An ECS override on
- (Low) SubmitJob accepted an INACTIVE revision. Fixed. After deregistering
jd:1, bothjd:1and the full ARN with:1return ClientException.jd:2and barejdstill work. - (Low) Replayed events carried
exitCode: 0. Fixed. On plainserve, an EventBridge rule with an SQS target received RUNNABLE, STARTING and RUNNING with noexitCode, and SUCCEEDED withexitCode: 0. - (Low) terminate-job was hard to reach on a RUNNING job. Fixed. RUNNING now lasts 5s (
DefaultBatchJobRunDwell, stored per job asRunPhase, and old snapshots fall back toPhase). I polled to RUNNING andterminate-jobgave FAILED,kill it,isTerminated: true, withstartedAtandstoppedAtset.cancel-jobon a RUNNING job left it RUNNING. - (Low, pre-existing) Multinode job definitions drifted in Terraform. Fixed. No EC2 default is echoed now. In Terraform,
aws_batch_job_definitionwithtype = "multinode"and noplatform_capabilitiesplans clean. - (Low) ListJobs with arrayJobId ignored filters. Fixed.
--filters name=JOB_NAME,values=nomatchreturns 0 children andarr*returns 3. - (Low) Scheduling-policy binding could race with a delete. Fixed.
policyBindMunow covers create, update and delete.TestSchedulingPolicyBindingIsAtomicpasses under-race -count=2. With the CLI, deleting a policy in use returns ClientException, and once the queue is gone the delete succeeds.
What I checked
go build ./...ok.go vetok onproviders/aws/batch,server/aws/batch,services/batch,internal/settleandproviders/aws.go test -count=1passes for those packages and forpersist.go test -race -count=2passes forproviders/aws/batch,server/aws/batchandinternal/settle.gofmtis clean,go mod tidyleaves no diff, andgo generateleaves no diff.- I ran golangci-lint 2.14.0 on the touched packages without
--new-from-revand compared it with a merge-base worktree. The base has 22 issues and the head has 30. Of the 8 new ones, 7 are goconst and 1 is gosec G115 (see Low). - Terraform with hashicorp/aws 6.66.0, against
serve --async-settle. I used a Fargate compute environment, a fair-share scheduling policy, a fair-share queue, a FIFO queue, a container definition and a multinode definition.- apply, then
plan -detailed-exitcodereturned 0. - I updated
weight_factorand the queuepriority, applied, and plan returned 0 again. - destroy, then follow-up describes return empty.
- apply, then
- AWS CLI v2, with the queue, policy and lifecycle runs described above. A fair-share queue without
shareIdentifierreturns ClientException, and withAthe job submits. - CI is green and the PR is mergeable. The PR body and the new commit have no AI attribution.
High
None.
Medium
None.
Low
- A node override that targets part of a definition range is rejected (
providers/aws/batch/overrides.go:552).applyNodePropertyOverrideonly applies to a definition range that lies wholly inside the override'stargetNodes.- Take a definition with
targetNodes "0:"andnumNodes 2. An override withtargetNodes "0:0"(main node only) matches no range, so SubmitJob fails with "doesn't cover any node range". - Real Batch takes a sub-range override and applies it to those nodes.
- Either apply it to any overlapping range, which fits because the emulator runs no per-node workload, or list the limit in
nongoals/batch.md.
- containerOverrides is still dropped on ECS, EKS and multinode definitions (
providers/aws/batch/jobs.go:396).mergeContainerreturns nil when the definition has nocontainerProperties, so--container-overrideson anecsPropertiesdefinition is accepted and then disappears.- The new override paths reject a mismatch like this, so the same check here would keep the behavior consistent.
- Lint: gosec G115 on
int32(idx)(providers/aws/batch/job_lifecycle.go:538), plus 7 new goconst hits injobs.goandoverrides.go(environment,resourceRequirements,containerOverrides,instanceTypes, ...). G115 can't trigger, becauseidxis below the array size, which is at most 10,000. A bounded conversion or a//nolint:gosecwith a reason would clear it, and the strings could become constants.
| _ = json.Unmarshal(r["targetNodes"], &rt) | ||
|
|
||
| rr, ok := parseTargetNodes(rt, def.NumNodes) | ||
| if !ok || rr.start < or.start || rr.end > or.end { |
There was a problem hiding this comment.
A definition range is only matched when it lies wholly inside the override's targetNodes. With a definition range 0: (numNodes 2), an override targetNodes: "0:0" matches nothing, so SubmitJob returns "doesn't cover any node range". Real Batch applies a sub-range override to those nodes. Since there are no per-node workloads here, applying it to any overlapping range (or documenting the limit in nongoals) would avoid rejecting a valid request.
| // containerProperties: environment variables and resource requirements are | ||
| // merged (by name / type), every other override replaces the property. | ||
| func mergeContainer(props, overrides json.RawMessage) (json.RawMessage, error) { | ||
| if !isSet(props) { |
There was a problem hiding this comment.
When the definition has no containerProperties (ECS, EKS or multinode), this returns nil and containerOverrides is dropped without any error. The new ECS, EKS and node override paths reject a mismatch, so a matching ClientException here would make the behavior consistent.
| v = o.childViews(rec)[idx] | ||
| j.JobID = rec.Job.JobID + ":" + strconv.Itoa(idx) | ||
| j.JobARN = rec.Job.JobARN + ":" + strconv.Itoa(idx) | ||
| i := int32(idx) |
There was a problem hiding this comment.
golangci-lint 2.14 flags this as gosec G115 (int to int32). It can't overflow because idx is below the array size, which is at most 10,000, but a bounded conversion or a //nolint:gosec // idx < array size <= 10000 would keep the touched packages lint-clean.
…n type check and job state-change events Add SubmitJob, DescribeJobs, ListJobs, CancelJob and TerminateJob. Jobs move SUBMITTED -> PENDING -> RUNNABLE -> STARTING -> RUNNING -> SUCCEEDED or FAILED on the provider clock, honour dependsOn and array jobs, and follow the documented cancel vs terminate rules. SubmitJob validates the queue (exists, ENABLED), the job definition revision, dependencies, share identifier, array size, priority, timeout and tags. ListJobs supports the documented filters and pages with a stable cursor. Add scheduling-policy CRUD and validate schedulingPolicyArn on CreateJobQueue/UpdateJobQueue. Enforce the RegisterJobDefinition type enum (container, multinode) and the properties each type requires. Jobs and scheduling policies are taggable, and tags follow the TagResource limits and the reserved aws: prefix. Publish "Batch Job State Change" events to the EventBridge default bus from the shared serve scheduler (config.Tickable) and on job API calls. New state is included in snapshots. Part of stackshy#1496
Parse the array child index with strconv.ParseInt(..., 10, 32) so the value converted into ArrayProperties.Index is bounded at parse time (CodeQL go/incorrect-integer-conversion). lookup already rejected any index at or above the array size, so no out-of-range value reached the conversion; this makes the bound explicit.
- SubmitJob merges ecsPropertiesOverride, eksPropertiesOverride and nodeOverrides into the job detail instead of dropping them, and rejects an override the job definition can't take (wrong properties kind, unknown container name, numNodes without an open-ended range, targetNodes covering no range). - SubmitJob refuses an explicit INACTIVE revision (name:revision or ARN). - Replayed intermediate "Batch Job State Change" events no longer carry the final container exitCode. - With async settle a job stays RUNNING for 5s (DefaultBatchJobRunDwell) so a real client can terminate a running job; the dwell is stored on the job record and survives snapshots. - ListJobs with arrayJobId applies the JOB_NAME and other filters to the children. - Scheduling-policy binding (CreateJobQueue, UpdateJobQueue) and DeleteSchedulingPolicy are serialized so a queue can't end up bound to a deleted policy. - RegisterJobDefinition stores platformCapabilities as sent instead of echoing a default EC2, which made Terraform multinode definitions drift.
689b550 to
db8553e
Compare
Summary
Part of #1496. Closes all four AWS Batch items:
schedulingPolicyArnon CreateJobQueue/UpdateJobQueue.typeenum.Issue items
providers/aws/batch/jobs.go,job_lifecycle.go. Gap confirmed on the base:services/batch/driver/driver.gohad no job operations andserver/aws/batch/handler.godid not route/v1/submitjob(it was not inknownOps).schedulingPolicyArnfree textproviders/aws/batch/scheduling_policy.go,job_queue.go. On the base,CreateJobQueuestoredin.SchedulingPolicyARNverbatim and no scheduling-policy operations existed.typeenumproviders/aws/batch/job_definition.go(validateJobDefinitionType). On the base onlytype == ""was rejected, sotype=batchregistered. The pinned SDK (service/batchv1.74.0,types/enums.go) models onlycontainerandmultinode.internal/awsevents(the same seam EC2/ECS/SFN/Glue/SSM/SageMaker use), wired inproviders/aws/aws.gowireLifecycleEvents. AWS documents only "Batch Job State Change" (plus "Batch Job Queue Blocked", which cannot happen here because jobs never wait for capacity). It documents no compute-environment or job-queue state-change event, so none is emitted.What real AWS does (sources)
-/_; jobQueue by name or ARN; jobDefinition asname,name:revisionor ARN with or without the revision, where no revision means the latest ACTIVE one; array size 2 to 10,000; at most 20 dependencies;SEQUENTIALwithout a job ID andN_TO_Nfor array jobs;shareIdentifierrequired on a fair-share queue and not allowed on a FIFO queue;schedulingPriorityOverride0 to 9999; timeout at least 60 seconds; at most 50 tags.state: a DISABLED queue accepts no new jobs. ItsschedulingPolicyArntext says a fair-share policy "can be replaced but can't be removed", and a FIFO queue "can't have a fair-share scheduling policy added".reasonis required, at most 1024 characters. TerminateJob: STARTING or RUNNING jobs go to FAILED, and jobs that haven't reached STARTING are cancelled.*prefix match. JOB_DEFINITION takes a name (any revision), aprefix*, or an ARN (exact revision). BEFORE/AFTER_CREATED_AT take epoch milliseconds. Results are newest first. maxResults is at least 1, with up to 1000 per page by status and 100 with a filter.arnsrequired, up to 100), ListSchedulingPolicies (maxResults 1 to 100), UpdateSchedulingPolicy, DeleteSchedulingPolicy ("You can't delete a scheduling policy that's used in any job queues"). FairsharePolicy: computeReservation 0 to 99, shareDecaySeconds up to 604800. ShareAttributes: shareIdentifier is required and up to 255 characters plus an optional*; weightFactor is 0.0001 to 999.9999. Only one of fairsharePolicy and quotaSharePolicy can be set, and the kind can't be changed.type:container | multinode.containerrequires containerProperties, ecsProperties or eksProperties;multinoderequires nodeProperties.aws:prefix is reserved (tag naming rules).aws.batch, detail-typeBatch Job State Change, resources[jobArn], and a detail shaped like the DescribeJobs JobDetail. "Events aren't created for the initial job submission."Changes
services/batch/driver): 10 new operations (scheduling policies and jobs) with their types.providers/aws/batch):job_lifecycle.go):statusReason,startedAtandstoppedAtare derived on every read from the provider clock, the wayinternal/settleworks. Nothing runs on goroutines or timers.--async-settle(config.WithAsyncSettle) SUBMITTED, RUNNABLE and STARTING last 1s each (settle.DefaultBatchJobPhase) and RUNNING lasts 5s (settle.DefaultBatchJobRunDwell), so a real client can terminate a running job. Without it a job is SUCCEEDED as soon as its dependencies are, which is the same default as every other settling resource.dependsOnholds a job in PENDING until its dependencies finish, and fails it withDependent Job failedif one of them failed.<id>:<index>).SEQUENTIALruns the children one at a time,N_TO_Npairs child indexes, and the parent reports PENDING, then SUCCEEDED or FAILED, withstatusSummary.Essential container in task exitedand containerexitCode: 0.containerOverridesare merged into the definition'scontainerProperties: environment by name, resourceRequirements by type, and every other field replaced.ecsPropertiesOverrideandeksPropertiesOverrideare merged per container (matched by name), andnodeOverridesreplacenumNodesand merge into the node ranges theirtargetNodescover. An override the job definition can't take is a ClientException.jobs.go):isCancelled). A terminated running job keeps itsstartedAt.shareIdentifier, the array size, priority, timeout and tags.scheduling_policy.go): full CRUD with the FairsharePolicy and ShareAttributes bounds. A queue must reference an existing policy, a policy can be replaced but not removed, a FIFO queue can't gain one, and an in-use policy can't be deleted. Binding a policy to a queue and deleting a policy are serialized, so a queue can't end up bound to a deleted policy.container/multinodeenum and the properties each type requires.platformCapabilitiesis stored as sent; noEC2default is echoed (that made Terraform multinode definitions drift).tags.go): jobs and scheduling policies are taggable. All create calls and TagResource now enforce the 50/128/256 limits and the reservedaws:prefix. A TagResource that would push the merged set past 50 is rejected without changing anything.events.go):config.Tickableand is registered inProvider.Tickables(). A job nobody polls still reports each transition. Batch job API calls also publish, so library mode without a ticker still sees events.jobsMu, so an event target that calls back into Batch can't deadlock (covered byTestConcurrentJobOperationsunder-race).server/aws/batch): REST-JSON routes for the 10 operations, translation only.docs/coverage/nongoals/batch.mdlists the limits below, andgo generate ./...output is committed.--enforce-auth(theIAMService() == "batch"path, as for the other REST services without per-operation checks), so the new operations are covered without extra wiring. Per-operation IAM for Batch would be a separate change, as it was for Lambda in feat(aws-lambda): op-level IAM authorization under --enforce-auth (AUTHZ-X1f) #1530.Provider Coverage
Checklist
go test -p 2 ./...)golangci-lint run --new-from-rev=upstream/development)cloudemu_test.go(no AWS Batch section there; covered by provider and SDK wire tests)go generate ./...) and committed the resultTest Plan
Provider tests (
providers/aws/batch, FakeClock plusconfig.WithAsyncSettle). These don't compile on the base, where the operations don't exist:TestSubmitJobSyncSucceedsImmediately,TestJobLifecycleAdvancesWithClock(SUBMITTED→RUNNABLE→STARTING 1s each, RUNNING 5s, then SUCCEEDED)TestSubmitJobValidation,TestSubmitJobUsesLatestActiveRevision,TestDependentJobWaitsAndFailsWithDependencyTestCancelVersusTerminate,TestArrayJobChildren,TestListJobs,TestListJobsCursorIsStableTestJobStateChangeEvents,TestJobEventsFollowAsyncLifecycle,TestTickPublishesUnpolledTransitionsTestSchedulingPolicyCRUDAndQueueBinding,TestRegisterJobDefinitionTypeValidation,TestTagValidationTestJobsAndPoliciesSurviveSnapshot,TestConcurrentJobOperationsTestSubmitJobAppliesEcsPropertiesOverride,TestSubmitJobAppliesEksPropertiesOverride,TestSubmitJobAppliesNodeOverrides,TestSubmitJobRejectsUnusableOverrides,TestSubmitJobRejectsInactiveRevision,TestReplayedEventsCarryNoExitCode,TestListArrayChildrenAppliesFilters,TestSchedulingPolicyBindingIsAtomic,TestMultinodeJobDefinitionEchoesNoDefaultPlatform, and SDKTestSDKSubmitJobEcsPropertiesOverrideproviders/awsTestTickablesRegistersTimeDrivenServices(updated for Batch)SDK wire tests (
server/aws/batch, real aws-sdk-go-v2service/batchv1.74.0). These fail on the base, which does not serve these operations:TestSDKJobLifecycle,TestSDKArrayJob,TestSDKSchedulingPolicies,TestSDKRegisterJobDefinitionInvalidTypeTestSDKListJobsPaginatorAndFilters(SDKListJobsPaginator),TestSDKReservedTagPrefixRejectedResults:
go build ./...: ok.go veton the touched packages: ok.go test -p 2 ./...: all packages pass.go test -race -count=1 ./providers/aws/batch/ ./server/aws/batch/: ok.TestConcurrentJobOperationsalso passes with-race -count=2.golangci-lint run --concurrency 2 --allow-parallel-runners --new-from-rev=upstream/developmentonproviders/aws/batch,server/aws/batch,services/batch,internal/settleandproviders/aws: 0 issues.go generate ./...leaves no diff after the commit.Real-user run (AWS CLI 1.x with botocore 1.42, against
cloudemu serve --providers aws --aws-port 4601 --async-settle)The setup created an EventBridge rule (
{"source":["aws.batch"],"detail-type":["Batch Job State Change"]}) with an SQS target, then a Fargate compute environment, queueqand job definitionjd.Job lifecycle:
submit-job --job-name hello --container-overrides '{"command":["echo","bye"]}', thendescribe-jobsonce a second, returned SUBMITTED, RUNNABLE, STARTING, then SUCCEEDED withEssential container in task exited,exitCode 0,command ["echo","bye"], and the queue and job definition as ARNs. RUNNING fell between two polls because of CLI latency.--depends-on jobId=<parent>: the parent was STARTING while the child was PENDING, and both later SUCCEEDED.Cancel vs terminate:
cancel-jobon a RUNNABLE job returned FAILED,user cancel,isCancelled: true.cancel-jobon a RUNNING job had no effect (exit 0).terminate-jobon the same RUNNING job returned FAILED,kill it,isTerminated: true, withstartedAtandstoppedAtset.terminate-jobon a SUBMITTED job returned FAILED,isCancelled: true.cancel-job --job-id does-not-existreturned ClientException.Array jobs:
--array-properties size=3: the parent was PENDING withstatusSummarySTARTING: 3.list-jobs --array-job-id <id> --job-status SUCCEEDEDlisted<id>:0..2with their indexes.list-jobs:
--job-status SUCCEEDEDlisted the jobs newest first.--job-status FAILEDshowedtocancel user cancel.--filters name=JOB_NAME,values=HEL*returnedhello.name=JOB_DEFINITION,values=jd --max-items 2paginated.--job-status DONE, two filters at once, and both--job-queueand--array-job-id.Validation errors (all ClientException, exit 255):
submit-jobwith:jd:99--job-name=-x--job-name "has space"--array-properties size=1--depends-on jobId=nope--share-identifier Aon a FIFO queue--tags aws:team=xregister-job-definitionwith:--type batch--type CONTAINER--type multinodewithout nodePropertiesScheduling policies:
create-scheduling-policy --name fair --fairshare-policy {...} --tags k=vsucceeded.fairagaincomputeReservation: 100weightFactor: 1000update-scheduling-policyon a missing ARNdelete-scheduling-policyon a missing ARNtag-resource --tags aws:x=ydescribe-scheduling-policiesandlist-scheduling-policiesround-tripped the policy.describe-scheduling-policieswith a missing ARN returned[].update-scheduling-policyupdated it.create-job-queue --scheduling-policy-arn .../nopereturned ClientException. With the real ARN it succeeded.update-job-queue --job-queue q --scheduling-policy-arn ...on the FIFO queue returned ClientException.submit-jobwithout--share-identifierreturned ClientException, and with--share-identifier A1it succeeded.delete-scheduling-policywhile the policy was in use returned ClientException.EventBridge (received through the SQS target):
hello: RUNNABLE, STARTING, RUNNING, SUCCEEDED, with no event for the submission.child: PENDING, RUNNABLE, STARTING, RUNNING, SUCCEEDED.tocancel: FAILED.arr: PENDING, SUCCEEDED.aws.batch, detail-typeBatch Job State Change, and resources[jobArn]. The events for jobs nobody was polling arrived through the scheduler tick.Persistence: with
serve --persist --state-file state.json, I submitted a job taggedt=1and created policysp, stopped the server with SIGINT, and restarted it.list-jobs,list-tags-for-resource(t=1) andlist-scheduling-policiesall returned the same data.I stopped the server afterwards, and nothing is listening on 4601.
Terraform was not run by me; the reviewer ran hashicorp/aws 6.66.0 against it.
Review follow-up run (same CLI,
serve --async-settleon 4601, then plainservefor the event check):ecs-properties-override,eks-properties-overrideandnode-overridesshow up indescribe-jobs; an ECS override on a containerProperties definition and an unknown container name return ClientException;submit-job --job-definition jd:1after deregistering it returns ClientException whilejd:2works; polling caught a job in RUNNING andterminate-jobgave FAILED withisTerminated: trueandstartedAtkept;list-jobs --array-job-id X --filters name=JOB_NAME,values=nomatchreturns 0 children; a multinode definition withoutplatformCapabilitiesdescribes asnull; and through EventBridge to SQS without--async-settlethe RUNNABLE, STARTING and RUNNING events carry noexitCodewhile SUCCEEDED carriesexitCode: 0.Known limits and risks
attemptsis empty, andretryStrategyandtimeoutare stored and echoed but never applied.nodeDetails, andListJobs --multi-node-job-idreturns an empty list. Overrides aren't checked against compute-environment platform rules (for examplenodeOverrideson Fargate). Array children emit no events of their own.TagResourceand the create calls now reject anaws:tag key and more than 50 tags on compute environments, job queues and job definitions too. That matches the TagResource reference, but it is stricter than before for those three resources.--async-settlea job is SUCCEEDED at submission, the same as other settling resources. Users who want to watch the progression need--async-settle(CLOUDEMU_ASYNC_SETTLE).Related Issues
Part of #1496