🧼

Cloud Workflowsで自動リトライ(Exponential Backoff) を実装する(2) 〜 try/retryでリファクタ

2022/02/17に公開

前回の記事
https://zenn.dev/koshilife/articles/1261494f74dd07

こちらの記事の英語版をdev.toに投稿したところ、Cloud WorkflowsのPdMからtry/retry文を使うと、もっとシンプルに表現できるよ的な親切なコメントをもらいました。

https://dev.to/krisbraun/comment/1m4bj

そこで、try/retry文でリファクタしてみました。

(参考) 公式ドキュメント

https://cloud.google.com/workflows/docs/reference/syntax/retrying

デフォルトのリトライポリシーを使う

デフォルトポリシー http.default_retry を使ったパターン。

本記事執筆時点でのリトライ条件は HTTPステータスコード(429, 502, 503, 504)とConnectionError, TimeoutError エラーが対象。
バックオフ設定はリトライ回数=>5、初期待ち時間=>1s、乗数 1.25。

https://cloud.google.com/workflows/docs/reference/stdlib/http/default_retry
Retries on 429 (Too Many Requests), 502 (Bad Gateway), 503 (Service unavailable), and 504 (Gateway Timeout), as well as on any ConnectionError and TimeoutError.
Uses max_retries of 5, and backoff as per retry.default_backoff.

https://cloud.google.com/workflows/docs/reference/stdlib/retry/default_backoff
It has initial backoff of 1 second, max backoff of 1 minute, and a multiplier of 1.25.

前回の67行が14行になり、短く表現できました。

main:
    params: [input]
    steps:
    - call_api:
        try:
            call: http.get
            args:
                url: https://asia-northeast1-xxx.cloudfunctions.net/foobar
                auth:
                    type: OIDC
            result: api_result
        retry: ${http.default_retry}
    - return_value:
        return: ${api_result.body}

※ 冪等性が保証されない場合は、http.default_retry ではなく http.default_retry_non_idempotent の429,503エラーのみをリトライ対象にするポリシーも用意されています

カスタムのリトライポリシーを使う

カスタムポリシーを使ったパターン
前回と同じリトライ条件(429, 500エラー)、バックオフ設定(初期待ち時間:10秒, 乗数:2)。
前回の67行が29行になり、短く表現できました。

main:
    params: [input]
    steps:
    - call_api:
        try:
            call: http.get
            args:
                url: https://asia-northeast1-xxx.cloudfunctions.net/foobar
                auth:
                    type: OIDC
            result: api_result
        retry:
            predicate: ${retry_if_429_or_500}
            backoff:
                initial_delay: 10
                max_delay: 300
                multiplier: 2
    - return_value:
        return: ${api_result.body}

retry_if_429_or_500:
    params: [e]
    steps:
      - what_to_repeat:
          switch:
          - condition: ${("code" in e) and (e.code == 429 or e.code == 500)}
            return: True
      - otherwise:
          return: False

おまけ

おまけ1) リトライ回数を超えた場合=>最後のErrorオブジェクトがraiseされる

リトライ回数を超えたときの挙動を確認したときの検証コード。
try/retry文をtry/except文で囲むことで補足できます。

main:
    params: [input]
    steps:
    - initialize:
        assign:
            - count: 0
    - try_call_func:
        try:
            try:
                steps:
                    - increment_count:
                        assign:
                            - count: ${count + 1}
                    - log_before_call:
                        call: sys.log
                        args:
                            text: ${"call API. " + string(count) + " time(s)"}
                    - call_api:
                        call: http.get
                        args:
                            url: https://asia-northeast1-xxx.cloudfunctions.net/foobar
                            auth:
                                type: OIDC
                        result: api_result
                    - return_ok:
                        return: ${api_result.body}
            retry: ${http.default_retry}
        except:
            as: e
            steps:
                - log_error:
                    call: sys.log
                    args:
                        severity: 'ERROR'
                        json: ${e}

コンソール画面より

おまけ2) カスタムポリシー内でraiseした時の挙動=>そのままraiseされる

カスタムポリシーを書く際に引数で受けるerrorオブジェクトをハンドリングするわけだが、その中でraiseされたらどうなるのか挙動が知りたかったので検証してみた。
結果的にそのままraiseされるので、カスタムポリシー内のerrorオブジェクトのハンドリングも手抜きしてはいけない。

main:
    params: [input]
    steps:
    - call_api:
        try:
            call: http.get
            args:
                url: https://asia-northeast1-xxx.cloudfunctions.net/foobar
                auth:
                    type: OIDC
            result: api_result
        retry:
            predicate: ${just_raise}
            backoff: ${retry.default_backoff}
    - return_value:
        return: ${api_result.body}

just_raise:
    params: [e]
    steps:
    - log_error:
        call: sys.log
        args:
            severity: 'WARNING'
            json: ${e}
    - raise_error:
        raise: ${e}

コンソール画面より

おまけ3) http.default_retryのリトライ条件に500エラーも加えたい=>カスタムポリシーが必要

http.default_retry では 500エラーはリトライ条件に含まれていないので、Cloud Functions, Cloud Runでスケーリング問題で発生する500エラーもリトライ条件に加えたい場合は、カスタムポリシーを作る必要があります。

main:
    params: [input]
    steps:
    - call_api:
        try:
            call: http.get
            args:
                url: https://asia-northeast1-xxx.cloudfunctions.net/foobar
                auth:
                    type: OIDC
            result: api_result
        retry:
            predicate: ${custom_retry_policy}
            backoff: ${retry.default_backoff}
    - return_value:
        return: ${api_result.body}

custom_retry_policy:
    params: [e]
    steps:
    - assign_retry_codes:
        assign:
            - retry_codes: [429, 500, 502, 503, 504]
    - what_to_repeat:
        switch:
          - condition: ${("code" in e) and (e.code in retry_codes)}
            return: True
          - condition: ${("tags" in e) and ("ConnectionError" in e.tags)}
            return: True
          - condition: ${("tags" in e) and ("TimeoutError" in e.tags)}
            return: True
    - otherwise:
          return: False

Discussion