AWS Step Functions のステート定義におけるリトライの書き方

## 指数バックオフアルゴリズム 　
SFN(Step Functions) は指数バックオフによるリトライが行われます。
(エクスポネンシャルバックオフ)

https://docs.aws.amazon.com/ja_jp/general/latest/gr/api-retries.html

SFNにおける式はこんな感じ
`リトライ時間 = IntervalSeconds * BackoffRate ^ (Attempts-1)`

https://d1.awsstatic.com/webinars/jp/pdf/services/20190522_AWS-Blackbelt_StepFunctions.pdf#page=36

簡単に言うと、回数を追うごとに待ち時間が長くなっていくようなリトライアルゴリズムです。


いきなりこんな式を見てもピンと来ないので、エラー時に成功するまでn回リトライするようなケースを考えてみます。

### よくあるリトライ
* `リトライ間隔 = 2秒` (固定)
* `リトライ回数 = n回`

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/510540/760553f8-b2c5-5775-a705-c979962ddf67.png" width="80%" alt="nomal_retry.png">

リトライ間隔は常に一定で、n回リトライを行います。

### 指数バックオフによるリトライ
* `リトライ間隔 = 2秒` (初回のみ)
* `リトライ回数 = n回`
* `バックオフレート = 3秒`

![exp_retry.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/510540/95a3dcff-fa15-1994-2a2c-8782b1c08e30.png)

こちらは通常のリトライとは異なり、リトライ回数が増えるごとに、リトライ間隔が増えていきます。

### リトライ間隔の計算
* `リトライ間隔 = 2秒` (初回のみ)
* `リトライ回数 = 10回`
* `バックオフレート = 3秒`

リトライ間隔を計算するワンライナーのPython スクリプト
(python環境があるコンソールに貼り付ければ動きます)

```python
python -c "for s in (lambda i,m,b: (i*b**(n-1) for n in range(1, m+1)))(2,10,3): print(s)"
```
> 2  ・・・ 2 * 3^(1-1) = 2 * 3^0 = 2 * 1
6  ・・・ 2 * 3^(2-1) = 2 * 3^1 = 2 * 3
18  ・・・2 * 3^(3-1) = 2 * 3^2 = 2 * 9
54  ・・・2 * 3^(4-1) = 2 * 3^3 = 2 * 27
162  ・・・ 以下省略
486
1458
4374
13122
39366

### 総リトライ時間[s]
* `リトライ間隔 = 2秒` (初回のみ)
* `リトライ回数 = 10回`
* `バックオフレート = 3秒`

```python
python -c "print(sum((lambda i,m,b: (i*b**(n-1) for n in range(1, m+1)))(2,10,3)),'[s]')"
```
> 59048 [s]

### 総リトライ時間[h]
* `リトライ間隔 = 2秒` (初回のみ)
* `リトライ回数 = 10回`
* `バックオフレート = 3秒`

```python
python -c "print(sum((lambda i,m,b: (i*b**(n-1) for n in range(1, m+1)))(2,10,3))/3600,'[h]')"
```
> 16.40222222222222 [h]


## SFN での書き方
### 使用するステートマシン
特定のEC2インスタンスを起動するステートマシン
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/510540/6386fb3f-8ca4-db2b-db98-15eca017e8e2.png" width="300px" alt="example_sfn">

### 定義(JSON)
```json
{
  "Comment": "Start the EC2 instance.",
  "StartAt": "StartInstances",
  "States": {
    "StartInstance": {
      "Type": "Task",
      "Resource": "arn:aws:states:::aws-sdk:ec2:startInstances",
      "Parameters": {
        "InstanceIds": [
          "i-1234567890abcdef0"
        ]
      },
      "Retry": [
        {
          "ErrorEquals": [
            "States.ALL"
          ],
          "IntervalSeconds": 2,
          "MaxAttempts": 10,
          "BackoffRate": 3
        }
      ],
      "End": true
    }
  }
}
```

### 定義(YAML)
※Serverless Framework などでデプロイする時

```yaml
--- 
Comment: "Start the EC2 instance."
StartAt: "StartInstances"
States: 
  StartInstance: 
    Type: "Task"
    Resource: "arn:aws:states:::aws-sdk:ec2:startInstances"
    Parameters: 
      InstanceIds: 
        - "i-1234567890abcdef0"
    Retry: 
      - ErrorEquals: 
          - "States.ALL"
        IntervalSeconds: "2"
        MaxAttempts: "10"
        BackoffRate: "3"
    End: "true"
```

### ErrorEquals
このエラーに合致したものが、リトライの対象となります。

以下のようなものがデフォルトで用意されているっぽいですね
* States.Runtime
* States.Timeout
* etc...

https://docs.aws.amazon.com/ja_jp/step-functions/latest/dg/concepts-error-handling.html

Lambda なんかだと・・・

https://docs.aws.amazon.com/ja_jp/step-functions/latest/dg/bp-lambda-serviceexception.html

### ErrorEquals によるエラー判定 (独自実装の例外)
例えばPythonランタイムのLambdaで例外クラス`AccountAlreadyExistsException` をraise する

```python
class AccountAlreadyExistsException(Exception): pass

def create_account(event, context):
  raise AccountAlreadyExistsException("Account is in use!")
```

ステートマシンで`AccountAlreadyExistsException`をキャッチしたい場合、以下のように書く

```json
"ErrorEquals": ["AccountAlreadyExistsException"]
```

https://d1.awsstatic.com/webinars/jp/pdf/services/20190522_AWS-Blackbelt_StepFunctions.pdf#page=39