diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..2423ca0a6 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,13 @@ +# EditorConfig is awesome: https://EditorConfig.org +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[.py] +indent_size = 4 diff --git a/typescript/stepfunctions-job-poller/README.md b/typescript/stepfunctions-job-poller/README.md index d9e39e583..034104419 100644 --- a/typescript/stepfunctions-job-poller/README.md +++ b/typescript/stepfunctions-job-poller/README.md @@ -1,4 +1,5 @@ -# Stepfunctions Job Poller +# Step Functions Job Poller + --- @@ -6,24 +7,41 @@ > **This is an experimental example. It may not build out of the box** > -> This examples does is built on Construct Libraries marked "Experimental" and may not be updated for latest breaking changes. +> This examples is built on Construct Libraries marked "Experimental" and may not be updated for latest breaking changes. > -> If build is unsuccessful, please create an [issue](https://github.com/aws-samples/aws-cdk-examples/issues/new) so that we may debug the problem +> If build is unsuccessful, please create an [issue](https://github.com/aws-samples/aws-cdk-examples/issues/new) so that we may debug the problem --- -This example creates a basic Stepfunction workflow that starts a task and then listens at regular intervals for completion, then reports completion and status. +This example creates a basic Step Function workflow that starts a task and then listens at regular intervals for completion, then reports completion and status. + + + + + +Tasks: +1. `Start` +2. `Submit Lambda`: Invoke Submit Lambda which sets status +3. `Wait X Seconds`: Wait +4. `Get Job Status`: Invoke Check Status Lambda which validates status +5. `Job Complete?`: Choice: Read `"status"` of payload + a. If `"status"` is `"SUCCEEDED"` then continue to `#7 Get Final Job Status` task + b. If `"status"` is `"FAILED"` then continue to `#6 Job Failed` task + c. else return to task `#3 Wait X Seconds` +6. `Job Failed`: Define a Fail state in the state machine. +7. `Get Final Job Status`: Invoke Check Status Lambda +8. `End` ## Build To build this app, you need to be in this example's root folder. Then run the following: ```bash -npm install -g aws-cdk -npm install -npm run build +$ npm install -g aws-cdk +$ npm install +$ npm run build ``` This will install the necessary CDK, then this example's dependencies, and then build your TypeScript files and your CloudFormation template. diff --git a/typescript/stepfunctions-job-poller/index.ts b/typescript/stepfunctions-job-poller/index.ts index 20d4d9c79..72ab16376 100644 --- a/typescript/stepfunctions-job-poller/index.ts +++ b/typescript/stepfunctions-job-poller/index.ts @@ -1,50 +1,91 @@ -import cdk = require('@aws-cdk/core'); -import sfn = require('@aws-cdk/aws-stepfunctions'); -import sfn_tasks = require('@aws-cdk/aws-stepfunctions-tasks'); +import * as cdk from '@aws-cdk/core'; +import * as lambda from '@aws-cdk/aws-lambda'; +import * as sfn from '@aws-cdk/aws-stepfunctions'; +import * as tasks from '@aws-cdk/aws-stepfunctions-tasks'; +import * as events from '@aws-cdk/aws-events'; +import * as targets from '@aws-cdk/aws-events-targets'; +import * as fs from 'fs' class JobPollerStack extends cdk.Stack { - constructor(scope: cdk.App, id: string, props: cdk.StackProps = {}) { - super(scope, id, props); - - const submitJobActivity = new sfn.Activity(this, 'SubmitJob'); - const checkJobActivity = new sfn.Activity(this, 'CheckJob'); - - const submitJob = new sfn.Task(this, 'Submit Job', { - task: new sfn_tasks.InvokeActivity(submitJobActivity), - resultPath: '$.guid', - }); - const waitX = new sfn.Wait(this, 'Wait X Seconds', { - time: sfn.WaitTime.secondsPath('$.wait_time') - }); - const getStatus = new sfn.Task(this, 'Get Job Status', { - task: new sfn_tasks.InvokeActivity(checkJobActivity), - inputPath: '$.guid', - resultPath: '$.status', - }); - const isComplete = new sfn.Choice(this, 'Job Complete?'); - const jobFailed = new sfn.Fail(this, 'Job Failed', { - cause: 'AWS Batch Job Failed', - error: 'DescribeJob returned FAILED', - }); - const finalStatus = new sfn.Task(this, 'Get Final Job Status', { - task: new sfn_tasks.InvokeActivity(checkJobActivity), - inputPath: '$.guid', - }); - - const chain = sfn.Chain - .start(submitJob) - .next(waitX) - .next(getStatus) - .next(isComplete - .when(sfn.Condition.stringEquals('$.status', 'FAILED'), jobFailed) - .when(sfn.Condition.stringEquals('$.status', 'SUCCEEDED'), finalStatus) - .otherwise(waitX)); - - new sfn.StateMachine(this, 'StateMachine', { - definition: chain, - timeout: cdk.Duration.seconds(30) - }); - } + constructor(app: cdk.App, id: string) { + super(app, id); + + /** ------------------ Lambda Handlers Definition ------------------ */ + + const getStatusLambda = new lambda.Function(this, 'CheckLambda', { + code: new lambda.InlineCode(fs.readFileSync('lambdas/check_status.py', { encoding: 'utf-8' })), + handler: 'index.main', + timeout: cdk.Duration.seconds(30), + runtime: lambda.Runtime.PYTHON_3_6, + }); + + const submitLambda = new lambda.Function(this, 'SubmitLambda', { + code: new lambda.InlineCode(fs.readFileSync('lambdas/submit.py', { encoding: 'utf-8' })), + handler: 'index.main', + timeout: cdk.Duration.seconds(30), + runtime: lambda.Runtime.PYTHON_3_6, + }); + + /** ------------------ Step functions Definition ------------------ */ + + const submitJob = new tasks.LambdaInvoke(this, 'Submit Job', { + lambdaFunction: submitLambda, + // Lambda's result is in the attribute `Payload` + outputPath: '$.Payload', + }); + const waitX = new sfn.Wait(this, 'Wait X Seconds', { + /** + * You can also implement with the path stored in the state like: + * sfn.WaitTime.secondsPath('$.waitSeconds') + */ + time: sfn.WaitTime.duration(cdk.Duration.seconds(30)), + }); + const getStatus = new tasks.LambdaInvoke(this, 'Get Job Status', { + lambdaFunction: getStatusLambda, + outputPath: '$.Payload', + }); + + const jobFailed = new sfn.Fail(this, 'Job Failed', { + cause: 'AWS Batch Job Failed', + error: 'DescribeJob returned FAILED', + }); + + const finalStatus = new tasks.LambdaInvoke(this, 'Get Final Job Status', { + lambdaFunction: getStatusLambda, + outputPath: '$.Payload', + }); + + // Create chain + const definition = submitJob + .next(waitX) + .next(getStatus) + .next(new sfn.Choice(this, 'Job Complete?') + // Look at the "status" field + .when(sfn.Condition.stringEquals('$.status', 'FAILED'), jobFailed) + .when(sfn.Condition.stringEquals('$.status', 'SUCCEEDED'), finalStatus) + .otherwise(waitX)); + + // Create state machine + const stateMachine = new sfn.StateMachine(this, 'CronStateMachine', { + definition, + timeout: cdk.Duration.minutes(5), + }); + + // Grant lambda execution roles + submitLambda.grantInvoke(stateMachine.role); + getStatusLambda.grantInvoke(stateMachine.role); + + /** ------------------ Events Rule Definition ------------------ */ + + /** + * Run every day at 6PM UTC + * See https://docs.aws.amazon.com/lambda/latest/dg/tutorial-scheduled-events-schedule-expressions.html + */ + const rule = new events.Rule(this, 'Rule', { + schedule: events.Schedule.expression('cron(0 18 ? * MON-FRI *)') + }); + rule.addTarget(new targets.SfnStateMachine(stateMachine)); + } } const app = new cdk.App(); diff --git a/typescript/stepfunctions-job-poller/lambdas/check_status.py b/typescript/stepfunctions-job-poller/lambdas/check_status.py new file mode 100644 index 000000000..c9cbc18cd --- /dev/null +++ b/typescript/stepfunctions-job-poller/lambdas/check_status.py @@ -0,0 +1,5 @@ +def main(event, context): + if event["status"] == "SUCCEEDED": + return {"status": "SUCCEEDED", "id": event["id"]} + else: + return {"status": "FAILED", "id": event["id"]} diff --git a/typescript/stepfunctions-job-poller/lambdas/submit.py b/typescript/stepfunctions-job-poller/lambdas/submit.py new file mode 100644 index 000000000..baf198b51 --- /dev/null +++ b/typescript/stepfunctions-job-poller/lambdas/submit.py @@ -0,0 +1,7 @@ +def main(event, context): + print('The job is submitted successfully!') + # Return the handling result + return { + "id": event['id'], + "status": "SUCCEEDED", + } diff --git a/typescript/stepfunctions-job-poller/package.json b/typescript/stepfunctions-job-poller/package.json index 623a3e68b..805144b19 100644 --- a/typescript/stepfunctions-job-poller/package.json +++ b/typescript/stepfunctions-job-poller/package.json @@ -15,10 +15,13 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/node": "^10.17.0", - "typescript": "~3.7.2" + "@types/node": "*", + "typescript": "^3.8" }, "dependencies": { + "@aws-cdk/aws-events": "*", + "@aws-cdk/aws-events-targets": "*", + "@aws-cdk/aws-lambda": "*", "@aws-cdk/aws-stepfunctions": "*", "@aws-cdk/aws-stepfunctions-tasks": "*", "@aws-cdk/core": "*" diff --git a/typescript/stepfunctions-job-poller/step-function-graph.png b/typescript/stepfunctions-job-poller/step-function-graph.png new file mode 100644 index 000000000..83e1c0e29 Binary files /dev/null and b/typescript/stepfunctions-job-poller/step-function-graph.png differ diff --git a/typescript/stepfunctions-job-poller/tsconfig.json b/typescript/stepfunctions-job-poller/tsconfig.json index f2e82ef87..47bd56eb8 100644 --- a/typescript/stepfunctions-job-poller/tsconfig.json +++ b/typescript/stepfunctions-job-poller/tsconfig.json @@ -1,21 +1,27 @@ { - "compilerOptions": { - "target":"ES2018", - "module": "commonjs", - "lib": ["es2016", "es2017.object", "es2017.string"], - "strict": true, - "noImplicitAny": true, - "strictNullChecks": true, - "noImplicitThis": true, - "alwaysStrict": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "noImplicitReturns": true, - "noFallthroughCasesInSwitch": false, - "inlineSourceMap": true, - "inlineSources": true, - "experimentalDecorators": true, - "strictPropertyInitialization":false - } + "compilerOptions": { + "target": "ES2018", + "module": "commonjs", + "lib": [ + "es2016", + "es2017.object", + "es2017.string" + ], + "strict": true, + "noImplicitAny": true, + "strictNullChecks": true, + "noImplicitThis": true, + "alwaysStrict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": false, + "inlineSourceMap": true, + "inlineSources": true, + "experimentalDecorators": true, + "strictPropertyInitialization": false + }, + "exclude": [ + "cdk.out" + ] } -