Batch tasks in workflows
Using Batch tasks in workflows is a little more involved than with Lambda tasks. Lambdas integrate a more natively with step functions than batch, which has additional layer of infrastructure between. As a result, users must keep a few more things in mind when wanting to use Batch tasks.
pre-batch
and post-batch
Because Batch tasks cannot take the input payload natively like Lambda and must
redirect through S3, Cirrus provides a built-in Lambda task to assist. The
pre-batch
task takes an input payload, uploads it to S3, and returns an
output JSON payload with a url
key with the S3 payload URL as its value.
pre-batch
should always be run immediately proceeding a Batch task for this
purpose.
Similarly, Batch does not integrate returned payloads with step functions nearly
as well as Lambda, so Cirrus has a built-in post-batch
task to help with,
well, post-batch operations. Specifically, post-batch
can pull the Batch
payload from S3 and return that in the case of a successful Batch execution. In
the case of a Batch error, post-batch
will scrape the execution logs for an
exception or other error message, and raise it within the step function. This
helps “bubble” Batch task errors up to the step function. Errors can then be
handled within the step function semantics or used to fail the step function
execution with error context that can ultimately be pushed into the state
database, increasing error visibility and assisting with troubleshooting.
Therefore, like pre-batch
, a post-batch
step should always immediately
follow a Batch task step.
Put it all together with a parallel
block
Now, instead of simply being able to have a single step in a step function for a
Batch task, we end up with three steps. Because of the way they operate
together, we can think of the pre-batch
-> Batch task -> post-batch
triad as a single step from the perspective of error handling and retries. That
is, if we encounter an error anywhere in that set of three, we either want to
fail them all or retry them all together.
Enter the step function parallel
block. Step functions provide this control
primative to allow users to define one or more branches in a workflow that can
execute in parallel. Interestly for us, parallel
supports both Catch
and
Retry
policies for error handling, which provides us with the control we
need for Batch.
Minimal example
Let’s see an example of a workflow using parallel
to group the set of Batch
operations together. Notice how the example uses parallel
with only a single
branch defined, but that fits the Batch use-case perfectly.
name: '#{AWS::StackName}-batch-example
definition:
Comment: "Example workflow using parallel to make a 'batch group'"
StartAt: batch-group
States:
batch-group:
Type: Parallel
Branches:
- StartAt: pre-batch
States:
pre-batch:
Type: Task
Resource: !GetAtt pre-batch.Arn
Next: batch-task
Retry:
- ErrorEquals: ["Lambda.TooManyRequestsException", "Lambda.Unknown"]
IntervalSeconds: 1
BackoffRate: 2.0
MaxAttempts: 5
batch-task:
Type: Task
Resource: arn:aws:states:::batch:submitJob.sync
Parameters:
JobName: some-batch-job
JobQueue: "#{ExampleJobQueue}"
JobDefinition: "#{ExampleBatchJob}"
# Note that this passes the value of the `url` key in the step's
# input JSON to the job definition as the parameter `url`i.
Parameters:
url.$: "$.url"
Next: post-batch
Catch:
# Ensures we always go to post-batch to pull errors
- ErrorEquals: ["States.ALL"]
ResultPath: $.error
Next: post-batch
post-batch:
Type: Task
Resource: !GetAtt post-batch.Arn
# End of the branch, not the step function
End: True
Retry:
- ErrorEquals: ["Lambda.TooManyRequestsException", "Lambda.Unknown"]
IntervalSeconds: 1
BackoffRate: 2.0
MaxAttempts: 5
Next: publish
# Parallel output is always an array of the outputs from each branch.
# We can use the OutputPath selector to get output index 0 as we only
# have a single branch, so we don't pass an array as input to the
# next task.
OutputPath: $[0]
Retry:
# This policy will retry any errors a second time
- ErrorEquals: ["States.ALL"]
IntervalSeconds: 3
MaxAttempts: 2
Catch:
# If the branch fails more than twice we fail the workflow
- ErrorEquals: ["States.ALL"]
ResultPath: $.error
Next: failure
publish:
Type: Task
Resource: !GetAtt publish.Arn
End: True
Retry:
- ErrorEquals: ["Lambda.TooManyRequestsException", "Lambda.Unknown"]
IntervalSeconds: 1
BackoffRate: 2.0
MaxAttempts: 5
Catch:
- ErrorEquals: ["States.ALL"]
ResultPath: $.error
Next: failure
failure:
Type: Fail
Batch retries vs step function retries
Whenver possible, using the step function retry semantics over those provided by
Batch is preferred. While Batch retries can be used without having to manage the
additional complexity of the parallel
block, Batch retries regardless of
error type, while step function retries allow matching specific error types,
allowing users more granular control over when to retry or fail.
Additionally, retrying within the step function shows the retry as a separate step than the first. This makes it much more obvious to users investigating failures that a retry happened and what the initial error was. Batch retries are more or less hidden from the step functions.
For these reasons, the overhead of the parallel
block is worth the
investment.