How to Implement a Nested While Loop in AWS Step Functions

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

Introduction

About one and a half year has been passed since AWS Step Functions was introduced in November 2016. It improves the possibility of Serverless applications. Previously, when we, the developers, tried to build a complicated business logic in which many AWS Lambda functions interacted with each other, we needed to implement that logic by ourselves. However, by using AWS Step Functions, we can implement it with a simple JSON document (Amazon States language). The service serves the framework for building a complicated logic as simple as possible in the world of Serverless. It's very useful! I use it every day in our project.

In this post, I'd like to give you tips about the service particularly how to loop based on given boolean conditions. In other words, I'd like to introduce how to implement a nested while loop in AWS Step Functions.

Why do you want to talk about a nested loop?

Because it's useful but there is less information about it on the Internet! The Official AWS document explains how to implement a loop in AWS Step Functions, but it's about a flattened loop, not a nested one.

Anyway, let's look at this picture below:

This is the state machine of AWS Step Functions working in our project. We make it continue alerting while the given conditions are evaluating to true.

In the state machine, there are two concepts: notification counts, how many times the state machine should iterate over; and notification user counts, how many users it should notify.

The notification count is evaluated at the IsNotificationCountReached state. If the result of the evaluation is true, then the loop continues, otherwise it ends. The notification user count is evaluated at the IsNotificationUserCountReached state. If the result of the evaluation is true, then the loop transitions to the NotifyAlerts task, otherwise to the IsNotificationCountReached choice.

The point is, we make AWS Step Functions notify alerts to users to maximum notification counts.

How We Implement the Loop in AWS Step Functions

Let's take a closer look at the state machine.

The FetchNotificationCount Task

The function takes the initial values of the loop from AWS DynamoDB and pass them into following states. For example, if there are the values in a variable (e.g, item ), the function should return them like this:

return {
    'notification_count': item['notification_count'],
    'notification_user_accounts': item['user_accounts'],
    'notification_user_count': len(item['user_accounts']),
    'is_first_alert': True
}

Then, following states can receive the data like this:

{
  "input": {
    "notification_count": 3,
    "notification_user_accounts": {
      "1": "foo@example.com",
      "2": "bar@example.com",
      "3": "baz@example.com"
    },
    "notification_user_count": 3,
    "is_first_alert": true
  }
}

AWS Lambda can reference the data with an event variable, or choice states can do that with a $ path.

The IsNotificationCountReached Choice

This is the condition evaluating how many times the loop iterates over. The time is saved in the notification_count variable, so we implement the logic below:

"IsNotificationCountReached": {
  "Type": "Choice",
  "Choices": [
    {
      "Variable": "$.notification_count",
      "NumericGreaterThan": 0,
      "Next": "IsFirstAlert"
    }
  ],
  "Default": "Done"
},

In this case, if the notification_count is greater than 0, the state machine transitions to next states. If not, it finishes its loop.

The IsFirstAlert Choice and the Sleep Wait

We make the state machine sleep for 5 minutes every loop except for the first loop. This prevents "noise alerts". We implement the logic like this:

"IsFirstAlert": {
  "Type": "Choice",
  "Choices": [
    {
      "Variable": "$.is_first_alert",
      "BooleanEquals": true,
      "Next": "DecrementNotificationUserCount"
    }
  ],
  "Default": "Sleep"
},
"Sleep": {
  "Type": "Wait",
  "Seconds": 300,
  "Next": "DecrementNotificationUserCount"
},

The state language expresses that if the is_first_alert variable is false, then it sleeps. If not, it goes to the next state immediately.

The DecrementNotificationUserCount Task

The main features of the task subtract 1 from the notification_user_count variable pass by preceding states and change the is_first_alert value to false. The code can be like this:

return {
    'notification_count': event['notification_count'],
    'notification_user_accounts': event['notification_user_accounts'],
    'notification_user_count': len(event['notification_user_accounts']) - 1,
    'is_first_alert': False
}

Following states can receive the data below:

{
  "input": {
    "notification_count": 3,
    "notification_user_accounts": {
      "1": "foo@example.com",
      "2": "bar@example.com",
      "3": "baz@example.com"
    },
    "notification_user_count": 2,
    "is_first_alert": false
  }
}

In fact, it's not necessary to implement those features in the task (e.g, the NotifyAlerts task can take the role). But, we prefer a simple function to a complicated one. So, for the present, we design the state machine like that.

The NotifyAlerts Task and the IsNotificationUserCountReached Choice

The NotifyAlerts task notifies alerts and the IsNotificationUserCountReached choice manages how many times the task notifies the users.

Let's look at the main features of the task. We make the task alert to Slack, but we will not touch on this matter. The task subtracts 1 from the notification_user_count variable every time when it notifies. If the value is -1, then the task recognizes all notifications have been sent to users and subtracts 1 from the notification_count variable. The main code can be like this:

notification_user_count = event['notification_user_count']
notification_count = event['notification_count']

if notification_user_count == -1:
    notification_count -= 1

output = event
output['notification_count'] = notification_count
output['notification_user_count'] = notification_user_count - 1

return output

Next, the IsNotificationUserCountReached choice. The notification_user_count variable has the information on which how many users the state machine should notify, so we implement the logic using the variable like this:

"IsNotificationUserCountReached": {
  "Type": "Choice",
  "Choices": [
    {
      "Variable": "$.notification_user_count",
      "NumericGreaterThanEquals": 0,
      "Next": "NotifyOverflow"
    }
  ],
  "Default": "IsNotificationCountReached"
},

The Whole of the State Machine

This is the state language described above. ARNs of AWS Lambda are masked.

{
  "Comment": "the state machine notifying alerts",
  "StartAt": "FetchNotificationCount",
  "States": {
    "FetchNotificationCount": {
      "Type": "Task",
      "Resource": "<aws-lambda-arn>",
      "Next": "IsNotificationCountReached"
    },
    "IsNotificationCountReached": {
      "Type": "Choice",
      "Choices": [
        {
          "Variable": "$.notification_count",
          "NumericGreaterThan": 0,
          "Next": "IsFirstAlert"
        }
      ],
      "Default": "Done"
    },
    "IsFirstAlert": {
      "Type": "Choice",
      "Choices": [
        {
          "Variable": "$.is_first_alert",
          "BooleanEquals": true,
          "Next": "DecrementNotificationUserCount"
        }
      ],
      "Default": "Sleep"
    },
    "Sleep": {
      "Type": "Wait",
      "Seconds": 300,
      "Next": "DecrementNotificationUserCount"
    },
    "DecrementNotificationUserCount": {
      "Type": "Task",
      "Resource": "<aws-lambda-arn>",
      "Next": "IsNotificationUserCountReached"
    },
    "IsNotificationUserCountReached": {
      "Type": "Choice",
      "Choices": [
        {
          "Variable": "$.notification_user_count",
          "NumericGreaterThanEquals": 0,
          "Next": "NotifyOverflow"
        }
      ],
      "Default": "IsNotificationCountReached"
    },
    "NotifyAlerts": {
      "Type": "Task",
      "Resource": "<aws-lambda-arn>",
      "Next": "IsNotificationUserCountReached"
    },
    "Done": {
      "Type": "Pass",
      "End": true
    }
  }
}

Conclusion

In this post, the implementation of a nested while loop in AWS Step Functions is discussed. I think it can be adopted in many situations. I hope you find this post helpful.