よくプログラムから外部のAPIにリクエストを投げるときに、たまにの接続エラーだったり、Timeoutだったりで、正常なレスポンスが返ってこないことがあります。
でも偶発的なエラーであれば、時間を空けて再試行することで正常な結果が得られることがあります。
私も新人のときに、少なくとも外部に通信をするときは、自動リトライの仕組みを設けようと教わりました。
ということで、今日のテーマはこちら!
本記事は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については以下の記事でもまとめています。
そして、実行結果は以下の通りです。ちゃんとリトライが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の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 ) )
のような式になっていると思われます。
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モジュールを導入したほうがメリットは大きいです。
リトライの仕組みを適切に設定して、安定したプログラムの実装を心がけましょう!