とりゅふの森

GCPデータエンジニアとして生きる

【Python入門】エラー時の自動リトライの仕組みを実装する

f:id:true-fly:20220301235251p:plain

よくプログラムから外部のAPIにリクエストを投げるときに、たまにの接続エラーだったり、Timeoutだったりで、正常なレスポンスが返ってこないことがあります。
でも偶発的なエラーであれば、時間を空けて再試行することで正常な結果が得られることがあります。
私も新人のときに、少なくとも外部に通信をするときは、自動リトライの仕組みを設けようと教わりました。
ということで、今日のテーマはこちら!

Pythonのエラー時の自動リトライの仕組みを実装する
  • 自動リトライの仕組みをループで実装しよう!
  • retryモジュールを使って自動リトライを設定しよう!
  • 本記事はPython 3.7.6で検証しています。

    今回のケース

    今回はシンプルな例で、以下のメソッド、call_api()が異常終了したときの自動リトライについて考えてみます。
    ご覧の通り、常に例外を返すだけのメソッドです。

    class ApiConnectionError(Exception):
        pass
    
    
    def call_api():
        raise ApiConnectionError('API接続エラー')
    

    自動リトライの仕組みをループで実装しよう!

    for文で試行回数繰り返すようにして、正常時はすぐにループから抜け、異常時は間隔を空けて繰り返し処理するようにしてみます。

    from time import sleep
    import logging
    
    # 処理時間を計測するため、loggerのフォーマット設定しています
    logger = logging.getLogger(__name__)
    logger.setLevel(logging.DEBUG)
    format = '%(asctime)s [%(levelname)s] %(filename)s, lines %(lineno)d. %(message)s'
    formatter = logging.Formatter(format, '%Y-%m-%d %H:%M:%S')
    stream_handler = logging.StreamHandler()
    stream_handler.setFormatter(formatter)
    logger.addHandler(stream_handler)
    
    class ApiConnectionError(Exception):
        pass
    
    def call_api(i):
        logger.info('APIコール')
        raise ApiConnectionError('API接続エラー')
    
    
    if __name__ == '__main__':
        # リトライ間隔(秒)
        retry_interval = 2
        # 試行回数
        tries = 4
        for i in range(0, tries):
            try:
                call_api(i)
                break
            except ApiConnectionError as e:
                if i + 1 == tries:
                    raise e
                sleep(retry_interval)
                logger.info('リトライ回数:{}回目'.format(i + 1))
                continue
    

    処理時間を計測するため、loggerのフォーマット設定していますが、loggerについては以下の記事でもまとめています。

    www.true-fly.com

    そして、実行結果は以下の通りです。ちゃんとリトライが3回、試行回数4回繰り返し、最後に異常終了していることがわかります。

    $ python main.py 
    2022-02-25 21:17:30 [INFO] main.py, lines 17. APIコール
    2022-02-25 21:17:32 [INFO] main.py, lines 47. リトライ回数:1回目
    2022-02-25 21:17:32 [INFO] main.py, lines 17. APIコール
    2022-02-25 21:17:34 [INFO] main.py, lines 47. リトライ回数:2回目
    2022-02-25 21:17:34 [INFO] main.py, lines 17. APIコール
    2022-02-25 21:17:36 [INFO] main.py, lines 47. リトライ回数:3回目
    2022-02-25 21:17:36 [INFO] main.py, lines 17. APIコール
    Traceback (most recent call last):
      File "main.py", line 45, in <module>
        raise e
      File "main.py", line 41, in <module>
        call_api(i)
      File "main.py", line 18, in call_api
        raise ApiConnectionError('API接続エラー')
    __main__.ApiConnectionError: API接続エラー
    (.venv)
    
    この書き方であればPython独自の文法だったり、ライブラリのインストールもいらずなので、 ほとんどのプログラミング言語でも応用できると思います。

    しかし割とコードが長くなりやすく、書き間違いなどで正しく動作しない可能性もあります。

    そこで次に試すのが、Pythonのretryモジュールです。

    retryモジュールを使って自動リトライを設定しよう!

    retryモジュールのキホン

    retryモジュールとは、その名の通りretryをいい感じにしてくれる外部ライブラリです。
    PYPIには、このretryモジュールのほか、retryingやTenacityといった外部ライブラリもあります。
    いずれもやりたいことは実現できそうなので、今回はこのretryモジュールで検証してみます。

    まずはライブラリのインストールから行います。

    pip install retry==0.9.2
    

    以下がretryモジュールを用いたサンプルコードです。

    import logging
    
    from retry import retry
    
    # 処理時間を計測するため、loggerのフォーマット設定しています
    logger = logging.getLogger(__name__)
    logger.setLevel(logging.DEBUG)
    format = '%(asctime)s [%(levelname)s] %(filename)s, lines %(lineno)d. %(message)s'
    formatter = logging.Formatter(format, '%Y-%m-%d %H:%M:%S')
    stream_handler = logging.StreamHandler()
    stream_handler.setFormatter(formatter)
    logger.addHandler(stream_handler)
    
    class ApiConnectionError(Exception):
        pass
    
    
    @retry(exceptions=ApiConnectionError, tries=4, delay=2)
    def call_api():
        logger.info('APIコール')
        raise ApiConnectionError('API接続エラー')
    
    
    if __name__ == '__main__':
        call_api()
    

    いかがでしょう?for文で実装したときよりも圧倒的にこちらのほうがシンプルですよね?
    retryモジュールは、@retry(ApiConnectionError, tries=4, delay=2)のように、メソッドにデコレータを追加することで設定ができます。
    retryデコレータのパラメータはそれぞれ以下を指定します。先程のfor文で実装したときと同等の設定にしています。

    • exceptions ・・・ 指定した例外が発生したときにリトライする
    • tries・・・試行回数
    • delay・・・リトライ間隔

    実行結果は以下の通りです。スタックトレースで、retryモジュールを通ってエラーが発生していることがわかるようになっていますね。

    $ python main.py
    2022-03-01 23:07:53 [INFO] main2.py, lines 20. APIコール
    2022-03-01 23:07:55 [INFO] main2.py, lines 20. APIコール
    2022-03-01 23:07:57 [INFO] main2.py, lines 20. APIコール
    2022-03-01 23:07:59 [INFO] main2.py, lines 20. APIコール
    Traceback (most recent call last):
      File "main.py", line 25, in <module>
        call_api()
      File "C:\work\articles\.venv\lib\site-packages\decorator.py", line 232, in fun
        return caller(func, *(extras + args), **kw)
      File "C:\work\articles\.venv\lib\site-packages\retry\api.py", line 74, in retry_decorator
        logger)
      File "C:\work\articles\.venv\lib\site-packages\retry\api.py", line 33, in __retry_internal
        return f()
      File "main.py", line 21, in call_api
        raise ApiConnectionError('API接続エラー')
    __main__.ApiConnectionError: API接続エラー
    

    backoffでリトライ間隔を調整する

    先程は指定した一定のリトライ間隔で試行していましたが、リトライ1回目、2回目、3回目と連れるに、間隔を広げていきたいケースもあります。その時は、retryデコレータのbackoffパラメータを指定することで、指数関数的に間隔を広げることができます。

    @retry(exceptions=ApiConnectionError, tries=10, delay=2, backoff=4)
    def call_api():
        logger.info('APIコール')
        raise ApiConnectionError('API接続エラー')
    

    実行結果は以下の通りです。

    $ python main.py
    2022-03-01 23:07:21 [INFO] main.py, lines 20. APIコール
    2022-03-01 23:07:23 [INFO] main.py, lines 20. APIコール
    2022-03-01 23:07:31 [INFO] main.py, lines 20. APIコール
    2022-03-01 23:08:03 [INFO] main.py, lines 20. APIコール
    2022-03-01 23:10:11 [INFO] main.py, lines 20. APIコール
    2022-03-01 23:18:43 [INFO] main.py, lines 20. APIコール
    Traceback (most recent call last):
      File "main.py", line 25, in <module>
        call_api()
      File "C:\work\articles\.venv\lib\site-packages\decorator.py", line 232, in fun
        return caller(func, *(extras + args), **kw)
      File "C:\work\articles\.venv\lib\site-packages\retry\api.py", line 74, in retry_decorator
        logger)
      File "C:\work\articles\.venv\lib\site-packages\retry\api.py", line 33, in __retry_internal
        return f()
      File "main.py", line 21, in call_api
        raise ApiConnectionError('API接続エラー')
    __main__.ApiConnectionError: API接続エラー
    

    リトライ間隔を指数関数的な曲線が描かれました。
    リトライ回数をx、リトライ間隔をy、delayをa、backoffをbとしたとき、
    y = a(b ^ ( x - 1 ) ) のような式になっていると思われます。

    f:id:true-fly:20220301232715p:plain:w480

    loggerを設定し、リトライ時にログ出力する

    retryデコレータのloggerパラメータにloggerを設定することで、リトライ時にログ出力することができます。

    @retry(exceptions=ApiConnectionError, tries=4, delay=3, backoff=2, logger=logger)
    def call_api():
        logger.info('APIコール')
        raise ApiConnectionError('API接続エラー')
    

    実行結果は以下の通りです。リトライ時にエラーメッセージとリトライ間隔がログ出力されます。

    $ python main.py 
    2022-03-01 23:40:04 [INFO] main.py, lines 20. APIコール
    2022-03-01 23:40:04 [WARNING] api.py, lines 40. API接続エラー, retrying in 3 seconds...
    2022-03-01 23:40:07 [INFO] main.py, lines 20. APIコール
    2022-03-01 23:40:07 [WARNING] api.py, lines 40. API接続エラー, retrying in 6 seconds...
    2022-03-01 23:40:13 [INFO] main.py, lines 20. APIコール
    2022-03-01 23:40:13 [WARNING] api.py, lines 40. API接続エラー, retrying in 12 seconds...
    2022-03-01 23:40:25 [INFO] main.py, lines 20. APIコール
    Traceback (most recent call last):
      File "main.py", line 25, in <module>
        call_api()
      File "C:\work\articles\.venv\lib\site-packages\decorator.py", line 232, in fun
        return caller(func, *(extras + args), **kw)
      File "C:\work\articles\.venv\lib\site-packages\retry\api.py", line 74, in retry_decorator
        logger)
      File "C:\work\articles\.venv\lib\site-packages\retry\api.py", line 33, in __retry_internal
        return f()
      File "main.py", line 21, in call_api
        raise ApiConnectionError('API接続エラー')
    __main__.ApiConnectionError: API接続エラー
    

    まとめ

    以上、今回はPythonでのリトライについて、ループで実装するパターンと、retryモジュールで実装するパターンの2パターンについてご紹介しました。
    実装の手軽さを考慮すると、特別ライブラリのインストールができない環境ではない限り、retryモジュールを導入したほうがメリットは大きいです。
    リトライの仕組みを適切に設定して、安定したプログラムの実装を心がけましょう!

    入門 Python 3 第2版

    入門 Python 3 第2版

    Amazon