iMind Developers Blog

iMind開発者ブログ

Pythonのretryingによるリトライ

概要

Pythonでは下記のように簡易なアノテーションで処理失敗時にリトライを行うことができるライブラリがいくつか存在する。

@retry()
def something():
    ''' retry until succeed'''

これらのライブラリの1つであるretryingを使ってみる。

バージョン情報

  • Python 3.6.5
  • retrying==1.3.3

導入

pip install retrying

成功するまで繰り返す

@retry() と記述すると成功するまでリトライし続ける。

import random
from retrying import retry

@retry()
def do_until_succeed():
    ''' ランダムの数値で0.1を超える値が出たらExceptionを投げる '''
    r = random.random()
    print(r)
    if r > 0.1:
        raise Exception()
    return r

do_until_succeed()

実行結果

0.81060856818656
0.8780613068499579
0.5715422307253717
0.27150813692025233
0.011350860015576836

成功する(ランダムが0.1以下の結果を返す)までリトライし続けている。

指定回数リトライする

stop_max_attempt_numberを指定すると、リトライする回数の上限が決められる。

@retry(stop_max_attempt_number=3)
def retry_3():
    r = random.random()
    print(r)
    if r > 0.1:
        raise Exception()
    return r

retry_3()

実行結果

0.9903434546113631
0.4168756887390249
0.3462867674416178
---------------------------------------------------------------------------
Exception

3回実行して成功しなかったのでExceptionがraiseされた。3回以内に成功すれば結果が返される。

指定時間リトライする

stop_max_delayを指定すると回数ではなく実行時間で打ち切ることもできる。

下記はstop_max_delayに3秒を指定し、Exceptionが起きるたびに1秒sleepさせている。3回くらい失敗したら諦めてExceptionがraiseされるはず。

import time

@retry(stop_max_delay=3000)
def retry_3sec():
    r = random.random()
    print(r)
    if r > 0.1:
        time.sleep(1) # 1秒待つ
        raise Exception()
    return r

実行結果

0.5683760114473412
0.8536491395488206
0.7305774618607249
---------------------------------------------------------------------------
Exception

試行のたびに時間を置く

ネットワーク周りの処理をリトライさせる時などは待たずに何度も試行するとDoS攻撃もどきになってしまう。

そうしたケースに使える指定時間待たせる指定も用意されている。

wait_fixedは固定時間待たせる。下記は1秒待たせる指定。

@retry(stop_max_attempt_number=5, wait_fixed=1000)
def retry_5_wait_1sec(start_time):
    print('{:.2f}sec'.format(time.time() - start_time)) #経過時間の表示
    raise Exception()

start_time = time.time()
retry_5_wait_1sec(start_time) 

実行結果

0.00sec
1.00sec
2.00sec
3.00sec
4.01sec
---------------------------------------------------------------------------
Exception

wait_random_minとwait_random_maxでmin〜maxの間のランダム時間waitさせることもできる。

下記は1〜2秒waitさせる指定を書いた上で、処理開始時から何秒経過したか表示している。

@retry(wait_random_min=1000, wait_random_max=2000)
def retry_wait_1s_to_2s(start_time):
    print('{:.2f}sec'.format(time.time() - start_time)) #経過時間の表示
    raise Exception()

start_time = time.time()
retry_wait_1s_to_2s(start_time) 

実行結果

0.00sec
1.54sec
2.77sec
4.53sec
6.53sec
7.79sec
9.74sec

wait時間がランダムになっていることがわかる。

徐々にwait時間を長くする

wait_incrementing_start(初期のwait時間)とwait_incrementing_increment(wait時間増分)を指定することで、徐々にwait時間を長くすることができる。

下記は1度目は100msecでリトライし、その後は+100msecずつwaitが伸びていく。

@retry(wait_incrementing_start=100, wait_incrementing_increment=100)
def retry_wait_increment(start_time):
    print('{:.2f}sec'.format(time.time() - start_time)) #経過時間の表示
    raise Exception()

実行結果

0.00sec
0.10sec
0.30sec
0.60sec
1.00sec
1.50sec

障害が起きてしばらくネットワークが不通になっている時などでこの手の指定はハマりそう。

上記のwait_incrementingは指定秒ずつwaitが伸びていくが、2乗で伸ばしていく設定もある。

@retry(wait_exponential_multiplier=100, wait_exponential_max=10000)
def wait_wait_exp_increment(start_time):
    print('{:.2f}sec'.format(time.time() - start_time)) #経過時間の表示
    raise Exception()

start_time = time.time()
wait_wait_exp_increment(start_time)

実行結果

0.00sec
0.20sec
0.60sec
1.40sec
3.00sec
6.21sec
12.62sec
22.63sec

200, 400, 800, 1600, 3200と2乗で時間が伸びていき、wait_exponential_maxで指定した秒数が最大値となる。

リトライ条件の指定

retry_on_exceptionでリトライする例外の条件を指定する。

下記はrandomに0〜2の値をとって、0なら0除算、1なら正常終了、2ならExceptionが発生する処理。retry_on_exceptionでZeroDivisionErrorの場合はTrueが返るようにしている。

def retry_if_zero_divide(exception):
    return isinstance(exception, ZeroDivisionError)

@retry(retry_on_exception=retry_if_zero_divide)
def divide():
    i = random.randint(0, 2)
    print(i)
    if i == 2:
        raise Exception()
    return 3 / i

divide()

初回が0(0除算)、次が1(正常終了)の場合、retry_if_zero_divideの指定に従い0除算がリトライされ、1で正常終了する。

0
1

実行結果が2の場合は、retry_if_zero_divideの指定に合わないExceptionが発生する為、リトライせずにそのままraiseされる。

2
---------------------------------------------------------------------------
Exception

リトライしたい条件の場合だけTrueが返るように関数に指定しておくことで、リトライ条件をコントロールすることができる。

retry_on_resultでは返り値によってリトライを指定できる。

下記は10を超える数字の場合はリトライすると指定している。

def retry_if_over_10(result):
    return result > 10

@retry(retry_on_result=retry_if_over_10)
def rand():
    i = random.randint(0, 100)
    print(i)
    return i

rand()

実行結果

98
35
61
53
91
10

ランダムで10を超える数字が出ている間はリトライされている。

例外のwrap

wrap_exception=Trueを指定すると例外がretrying.RetryErrorでwrapされて返される。

@retry(wrap_exception=True, stop_max_attempt_number=3)
def wrap_exception():
    raise Exception()

import retrying
try:
    wrap_exception()
except retrying.RetryError as e:
    print(e)
RetryError[Attempts: 3, Error:
  File "/home/user/local/miniconda3/lib/python3.6/site-packages/retrying.py", line 200, in call
    attempt = Attempt(fn(*args, **kwargs), attempt_number, False)
  File "retry_test.py", line 75, in wrap_exception
    raise Exception()
]

リトライ処理で失敗したことをプログラム側がcatchしたい場合などで使える。

改定履歴

Author: Masato Watanabe, Date: 2019-01-15, 記事投稿