AWS Step FunctionsのMapステート内でエラーが起きても全体を停止しない方法

こんにちは。サービス部の武田です。Step FunctionsのMapステートであるブランチがエラーになっても全体を停止しないようにしてみました。
2022.08.31

こんにちは。サービス部の武田です。

AWS Step Functionsはステートマシンとしてワークフローを定義し、効率よく実行できるサーバーレスオーケストレーションサービスです。Step FunctionsではMapという、入力配列を並行実行するためのステートが提供されています。

さてこのMapステートですが、普通に使用すると、あるブランチ(並行実行されている処理のうちのひとつ)がエラーなどで中断してしまうと、他のブランチも止まるという特徴があります。

例として次のようなステートマシンを定義してみます。

ASL(Amazon States Language)は次のようになっています。Passステートは次のMapステートに渡すための配列データを作っています。Mapステート内の処理は、値が3であれば3秒待機してFailステートで失敗する。それ以外であれば5秒待機してSuccessステートに遷移し成功する。という単純なものです。

state-machine-01.json

{
  "Comment": "A description of my state machine",
  "StartAt": "Pass",
  "States": {
    "Pass": {
      "Type": "Pass",
      "Next": "Map",
      "Result": {
        "values": [
          1,
          2,
          3,
          4,
          5
        ]
      }
    },
    "Map": {
      "Type": "Map",
      "End": true,
      "Iterator": {
        "StartAt": "Choice",
        "States": {
          "Choice": {
            "Type": "Choice",
            "Choices": [
              {
                "Variable": "$",
                "NumericEquals": 3,
                "Next": "3 Sec Wait"
              }
            ],
            "Default": "5 Sec Wait"
          },
          "3 Sec Wait": {
            "Type": "Wait",
            "Seconds": 3,
            "Next": "Fail"
          },
          "Fail": {
            "Type": "Fail"
          },
          "5 Sec Wait": {
            "Type": "Wait",
            "Seconds": 5,
            "Next": "Success"
          },
          "Success": {
            "Type": "Succeed"
          }
        }
      },
      "ItemsPath": "$.values"
    }
  }
}

実行してみると #2(=値が3) のブランチはFailステートに到達して失敗となっています。

一方で他のブランチはどうかというと、たとえば #1(=値が2) はキャンセルとなり、途中で処理が終わっています。

通常はこのように停止してしまった方がよいのですが、場合によってはあるブランチに左右されず、正常実行できるものは実行し切れた方がよい場合もあるでしょう。

途中でステートマシンを停止しないようにする

それでは先ほどのステートマシンを少し改変し、あるブランチのエラーで止まらないようにしてみましょう。改変後のステートマシンは次のようになります。

グラフだけではどう変わったのか分かりにくいですね。ASLは次のようになります。

state-machine-02.json

{
  "Comment": "A description of my state machine",
  "StartAt": "Pass",
  "States": {
    "Pass": {
      "Type": "Pass",
      "Next": "Map",
      "Result": {
        "values": [
          1,
          2,
          3,
          4,
          5
        ]
      }
    },
    "Map": {
      "Type": "Map",
      "End": true,
      "Iterator": {
        "StartAt": "Parallel",
        "States": {
          "Parallel": {
            "Type": "Parallel",
            "Branches": [
              {
                "StartAt": "Choice",
                "States": {
                  "Choice": {
                    "Type": "Choice",
                    "Choices": [
                      {
                        "Variable": "$",
                        "NumericEquals": 3,
                        "Next": "3 Sec Wait"
                      }
                    ],
                    "Default": "5 Sec Wait"
                  },
                  "3 Sec Wait": {
                    "Type": "Wait",
                    "Seconds": 3,
                    "Next": "Fail"
                  },
                  "Fail": {
                    "Type": "Fail"
                  },
                  "5 Sec Wait": {
                    "Type": "Wait",
                    "Seconds": 5,
                    "Next": "Success"
                  },
                  "Success": {
                    "Type": "Succeed"
                  }
                }
              }
            ],
            "Catch": [
              {
                "ErrorEquals": [
                  "States.ALL"
                ],
                "Next": "Catch Success"
              }
            ],
            "End": true
          },
          "Catch Success": {
            "Type": "Succeed"
          }
        }
      },
      "ItemsPath": "$.values"
    }
  }
}

ポイントは24行目のParallelステートおよび60行目のCatch定義です。Parallelステートは通常異なる処理を並行実行するためのステートですが、ここでは単なるコンテナ(入れ物)として使っています。CatchはこのParallelステートの中で何らかのエラーが起きた場合の例外処理を定義しています。ここではエラーが起きたら問答無用でSuccessステートになり処理を正常終了させています。

ではこのステートマシンを実行してみましょう。まずは先ほどステートマシンを停止する原因となっていた #2 です。Failステートに遷移して一度は失敗していますが、それがキャッチされ正常終了されています。

続いてキャンセルされていた #1 です。こちらは問題なくSuccessステートに遷移し正常実行できています。

まとめ

Mapステートのデフォルトの挙動では困る場面をエラーハンドリングを少し工夫することで回避できました。ただしエラーが起きた際の通知処理なども考慮すると、構成が複雑になることが懸念されます。その場合は無理にステートマシンの中で並列化するのではなく、そもそもステートマシンを複数回実行することなども検討するとよいでしょう。