retrying简介

retrying功能是为一些任务添加重试机制,只需要指定不同的参数就能便捷实现重试机制逻辑代码。

retrying特点

1.通用装饰器API
2.指定停止条件(即按尝试次数限制)
3.指定等待条件(即,尝试之间的指数退避休眠)
4.自定义异常重试
5.自定义对预期返回结果的重试

retrying安装

由于retrying是不包含在标准库上,需要使用额外安装:

pip install retrying

retrying参数解析

stop_max_attempt_number					指定重试的次数,默认是5
stop_max_delay							指定重试超时时间,默认是100,单位是ms
wait_fixed							    指定每次重试的时间间隔,默认是1000,单位是ms
wait_random_min=None           			指定每次重试最小时间,默认为0
wait_random_max=None,					指定每次重试最大时间,默认为1000
wait_incrementing_start			        指定每次重试递增的开始时间
wait_incrementing_increment				指定每次重试时间的递增幅度
wait_exponential_multiplier				指定每次重试时间的递增幅度,跟上面的参数算法差异
wait_exponential_max					指定每次重试时间的递增幅度的最大值
retry_on_exception 						指定每次出现异常执行的函数
retry_on_result							指定程序运行正常执行的函数
wrap_exception                       	出现异常后返回的异常类型,True是返回原异常,False返回RetryError
stop_func								自定义停止重试的条件,传入参数是一个函数
wait_func 								自定义每次重试的时间间隔,传入参数是一个函数
wait_jitter_max							指定每次重试时间抖动值
stop									指定重试停止后,执行Retrying对象的成员函数
wait									指定重试间隔,执行Retrying对象的成员函数

retrying简单例子

指定重试的次数或者重试的超时时间

import random
from retrying import retry


@retry(stop_max_attempt_number=5, stop_max_delay=2000)
def do_something_unreliable():
    rad = random.randint(0, 20)
    print("rad = %d" % rad)
    if rad > 1:
        raise IOError("Broken sauce, everything is hosed!!!111one")
    else:
        return "Awesome sauce!"


print(do_something_unreliable())

随机获取一个整形数rad = random.randint(0, 20),每次判断等于0则报错进行重试。当返回错误的时候则进行重试,重试超过5次或者重试超过两秒则停止重试,程序结束。

指定重试间隔时间

import random
from retrying import retry


@retry(wait_fixed=2000)
def do_something_unreliable():
    rad = random.randint(0, 20)
    print("rad = %d" % rad)
    if rad > 1:
        raise IOError("Broken sauce, everything is hosed!!!111one")
    else:
        return "Awesome sauce!"


print(do_something_unreliable())

@retry(wait_fixed=2000)指定每次重试的间隔为2秒,由于没有指定重试的限制,所以一直重试直到随机值等于0。

自定义函数的监听重试后的出现异常或者正常运行。

import random
from retrying import retry

def retry_on_result(value):
    print("程序正常返回结果")
    return False   #return True 就算返回正常结果还会继续不断重试

def retry_on_exception(e):
    print("出现异常")
    return True   #return False 重试中断

@retry(retry_on_result=retry_on_result, retry_on_exception=retry_on_exception)
def do_something_unreliable():
    rad = random.randint(0, 20)
    print("rad = %d" % rad)
    if rad > 1:
        raise IOError("Broken sauce, everything is hosed!!!111one")
    else:
        return "Awesome sauce!"


print(do_something_unreliable())

从示例程序看出retry_on_result是正常结果才调用的,retry_on_exception每次出现异常会调用。

retrying源码分析

retrying的源码只有200多行,比较容易理解,先看@retry

def retry(*dargs, **dkw):
    """
    Decorator function that instantiates the Retrying object
    @param *dargs: positional arguments passed to Retrying object
    @param **dkw: keyword arguments passed to the Retrying object
    """
    # support both @retry and @retry() as valid syntax
    #兼容@retry装饰器有参数和无参数的情况
    if len(dargs) == 1 and callable(dargs[0]):  
   		#无参数
        def wrap_simple(f):
			
            @six.wraps(f)
            def wrapped_f(*args, **kw):
                return Retrying().call(f, *args, **kw)

            return wrapped_f

        return wrap_simple(dargs[0])

    else:
    	#有参数
        def wrap(f):

            @six.wraps(f)
            def wrapped_f(*args, **kw):
                return Retrying(*dargs, **dkw).call(f, *args, **kw)

            return wrapped_f

        return wrap

retry函数首先判断@retry装饰器是否带参数,这个在上篇文章已经介绍过,这个不在重述。
然后直接调用Retrying对象的call函数

call函数

def call(self, fn, *args, **kwargs):
	#记录第一次重试的开始时间
    start_time = int(round(time.time() * 1000))
    #记录重试的次数
    attempt_number = 1
    while True:
        try:
        	#把传进来函数fn和参数封装到一个Attempt对象中,并执行fn函数
            attempt = Attempt(fn(*args, **kwargs), attempt_number, False)
        except:
        	#执行fn函数如果出现异常也会把异常封装成Attempt对象中
            tb = sys.exc_info()
            attempt = Attempt(tb, attempt_number, True)
		#判断是否存在异常,如果不存在则返回正常结果,
		#attempt.get()把得到fn(*args, **kwargs)的执行结果结束循环
        if not self.should_reject(attempt):
            return attempt.get(self._wrap_exception)
		#计算第一次重试到当次重试的时间差
        delay_since_first_attempt_ms = int(round(time.time() * 1000)) - start_time
        #判断重试是否达到停止的条件
        if self.stop(attempt_number, delay_since_first_attempt_ms):
        	#raise异常,程序重试结束
            if not self._wrap_exception and attempt.has_exception:
                # get() on an attempt with an exception should cause it to be raised, but raise just in case
                raise attempt.get()
            else:
                raise RetryError(attempt)
        else:
        	#如果重试没有被停止,则计算下一次重试的等待时间,也就是每次重试的间隔
            sleep = self.wait(attempt_number, delay_since_first_attempt_ms)
            if self._wait_jitter_max:
                jitter = random.random() * self._wait_jitter_max #1
                sleep = sleep + max(0, jitter)
            time.sleep(sleep / 1000.0)
		#重试次数累加1 
        attempt_number += 1

结合源码和注释,大致了解retrying核心的重试机制。并且出现了重试等待时间的第一个参数wait_jitter_max (在Retrying的构造函数被赋值到self._wait_jitter_max,因此如果传入wait_jitter_max 那会在原来时间上加random.random() * self._wait_jitter_max,值越大等待时间越大。

再看看self.stop(attempt_number, delay_since_first_attempt_ms)的代码,直接跳到Retrying的构造函数上:

########################## 省略了一部分代码#########################
 # TODO add chaining of stop behaviors
 # stop behavior
 stop_funcs = []
 #如果传入重试的停止次数stop_max_attempt_number,
 #则把self.stop_after_attempt成员函数加入到stop_funcs列表上
 if stop_max_attempt_number is not None:
     stop_funcs.append(self.stop_after_attempt)
     
 #如果传入重试的的超时时间stop_max_delay
 #则把self.stop_after_delay成员函数加入到stop_funcs列表上
 if stop_max_delay is not None:
     stop_funcs.append(self.stop_after_delay)

 #如果传入自定义停止重试函数stop_func 
 #则把stop_func函数赋值self.stop
 if stop_func is not None:
     self.stop = stop_func
     
 #如果没有传入stop_func,则遍历stop_funcs函数
 #这里使用lambda函数赋值给self.stop
 #any是一个内置函数,加入列表中有一个为True,则返回True
 #说明了只要满足stop_max_attempt_number 或者 stop_max_delay 的任意一个条件都会停止重试
 elif stop is None:
     self.stop = lambda attempts, delay: any(f(attempts, delay) for f in stop_funcs)

#把Retry的内置函数赋值给self.stop
 else:
     self.stop = getattr(self, stop)

通过这部分的注释可以对停止重试的参数有进一步的了解

接着看看self.wait(attempt_number, delay_since_first_attempt_ms)的代码,也是在Retrying的构造函数上:

# TODO add chaining of wait behaviors
 # wait behavior
  ########################## 省略了一部分代码#########################
 wait_funcs = [lambda *args, **kwargs: 0]
 if wait_fixed is not None:
     wait_funcs.append(self.fixed_sleep)

 if wait_random_min is not None or wait_random_max is not None:
     wait_funcs.append(self.random_sleep)

 if wait_incrementing_start is not None or wait_incrementing_increment is not None:
     wait_funcs.append(self.incrementing_sleep)

 if wait_exponential_multiplier is not None or wait_exponential_max is not None:
     wait_funcs.append(self.exponential_sleep)

 if wait_func is not None:
     self.wait = wait_func

 elif wait is None:
     self.wait = lambda attempts, delay: max(f(attempts, delay) for f in wait_funcs)

 else:
     self.wait = getattr(self, wait)

从源码可以看出,self.wait的处理逻辑跟self.stop是一致,却被就是函数列表加入不同的成员函数,这里不再详细解释。

剩下的参数可以自行查看代码,例如self.should_reject(attempt)会看到retry_on_exceptionretry_on_result这两参数的解释,总体来说retrying库的源码还是比较好理解的,重点还是在对装饰器的掌握上。

总结

retrying库只需要一行代码就可以实现重试机制逻辑,而且还可以指定停止条件、指定等待条件、.自定义异常重试和自定义对预期返回结果的重试。在使用上非常的便捷,能简化有特殊要求的代码结构。