你也可以手敲一个高速下载器(二)重试
你也可以手敲一个高速下载器(二)重试
前言
上节说过,高速下载器采用的多任务分段下载策略,所以要保证每个任务都能成功,所以就要有异常的重试。正常我们的操作是设置一个重试最大次数,然后发生异常的时候重新调用使用的方法,次数加一,直到成功或者达到最大次数。那么我们采用另一种做法:使用一个第三方的库:tenacity
基本使用
先看代码:
import random
from tenacity import retry
@retry
def test():
if random.randint(0, 10) > 1:
print("发生异常!!!")
raise IOError("Broken sauce, everything is hosed!!!111one")
else:
return "成功"
print(test())
# 运行结果
发生异常!!!
发生异常!!!
...
成功
上面的@retry
是最简单的调用,会在所装饰的方法发生异常时,重新调用该方法,知道成功为止,大家可以多运行几次,会发现输出: 发生异常!!!
的数量是不一样的。
停下来
但在实际应用中,如果发生了致命错误一直重试是不现实的,所以需要让他停下来,这时是可以使用:stop_after_attempt
,如下示例:
import random
from tenacity import retry, stop_after_attempt
@retry(stop=stop_after_attempt(4))
def test():
if random.randint(0, 10) > 1:
print("发生异常!!!")
raise IOError("Broken sauce, everything is hosed!!!111one")
else:
return "成功"
print(test())
# 运行结果
发生异常!!!
发生异常!!!
发生异常!!!
发生异常!!!
Traceback (most recent call last):
.....
tenacity.RetryError: RetryError[<Future at 0x2225e2f2dc0 state=finished raised OSError>]
大家会看到在4次之后重试就停止了,并且抛出了个重试错误的异常
等一会
在发生异常时可以选择等一会,等待几秒钟后在重试下一次,在时可以选择使用:wait_fixed、wait_random
,其中wait_fixed是等待固定时间,wait_random是等待随机的时间,示例如下:
import random
from tenacity import retry, stop_after_attempt, wait_fixed, wait_random
@retry(stop=stop_after_attempt(10), wait=wait_fixed(1)+wait_random(0, 1))
def test():
if random.randint(0, 10) > 1:
print("发生异常!!!")
raise IOError("Broken sauce, everything is hosed!!!111one")
else:
return "成功"
print(test())
# 结果略
在这里增加了一个wait
参数,后面值是:wait_fixed(1)+wait_random(0, 1))
,意思就是等待固定的一秒加随机是0 ~ 1秒,也就是说每次发生异常都会等待1 ~ 2秒
失败几次了
如果想在每次重试之后输出日志,可以使用:after
,如果想重试都结束了在打印日志可以使用:retry_error_callback
,示例代码如下:
import random
from tenacity import retry, stop_after_attempt, wait_fixed, wait_random
def after_callback(retry_state):
pring(f"第{retry_state.attempt_number}次重试失败")
def retry_error_callback(retry_state):
pring(f"{retry_state.attempt_number}次重试全部失败")
@retry(stop=stop_after_attempt(5), wait=wait_fixed(1)+wait_random(0, 1), after=after_callback, retry_error_callback=retry_error_callback)
def test():
if random.randint(0, 10) > 1:
print("发生异常!!!")
raise IOError("Broken sauce, everything is hosed!!!111one")
else:
return "成功"
print(test())
# 结果
发生异常!!!
第1次重试失败
发生异常!!!
第2次重试失败
发生异常!!!
第3次重试失败
发生异常!!!
第4次重试失败
发生异常!!!
第5次重试失败
5次重试全部失败
None
可以看到我们监听了所有的失败消息,倒时候可以在里面做更多的操作,为所欲为了,但还有一个问题,就第5次重试失败和5次重试全部失败其实是打印重复了,我们要的是在第五次的时候直接输出全部失败就可以了,并且我们是不知道异常的信息的,这时可以把after
和retry_error_callback
的回调函数改成一个,然后在里面输出异常信息,改造后的回调函数如下:
def retry_handler(self, retry_state: RetryCallState):
"""
处理重试之后和重试失败
:param retry_state:
:return:
"""
outcome: Future = retry_state.outcome
attempt_number = retry_state.attempt_number
exception_type = type(outcome.exception()).__name__
exception_msg = list(outcome.exception().args)
stack_msg = traceback.format_stack()[2].strip()
log_msg = f"[{self.method}] {self.url} 发生异常: [{exception_type}]: {exception_msg}"
if 'retry_error_callback' in stack_msg:
logger.error(f"{log_msg} {attempt_number}次重试全部失败")
raise Exception("失败")
else:
logger.error(f"{log_msg} 第{attempt_number}次重试")
在上面代码中,其实变量outcome
保存了重试的结果,当然异常的信息也会存在其中,retry_state.attempt_number
保存了当前重试的次数,下面两行就是获取异常的类型和异常的信息,然后大家可能会对下面的代码感到疑惑,因为我们是要区分当前是否全部重试结束,但只凭借参数retry_state
是没有办法(是我没办法了)做到的,所以只能剑走偏锋,因为在异常都结束的时候肯定会调用方法:retry_error_callback
,所以我们就可以根据当前的堆栈信息判断是否是最后一次,也就是这个函数的上一层是哪调的。
装饰器?用不了啊
众所周知,我们在写一个稍微正式点的项目的时候,都会用到面向对象的知识,也就是类的写法,然后我们在配置异常重试的时候,也都希望各个数值的可配置的,但配置在对象中的变量是无法在装饰器中使用的,因为装饰器中是取不到self
的,这就陷入了僵局。这时我们就要思考,既然装饰器用不了怎么办?其他办法可不可以?装饰器里面是咋实现的?可以看一下
我们可以看一下装饰器里面的源码,可以看到主要逻辑就是红色框框的地方,也就是会把我们输入的参数,全部输入下面的类中,具体使用哪个类则根据是不是异步来判断的。所有我们就可以在代码中直接使用异步重试类:
AsyncRetrying
,来代替装饰器来使用,比如下面:
r = AsyncRetrying(
stop=stop_after_attempt(self._setting.retrys_count),
after=self.retry_handler,
retry_error_callback=self.retry_handler,
wait=wait
)
resp = await r.wraps(self._request)(sem)
上面代码只是一部分而已,但就是这么个意思,理解就好
结语
这篇主要是简单而实用的讲了一个错误重试库:tenacity,当然,这个库的内容肯定不止我说的这么一点,更详细的大家一个去看官方的文档。我们下节继续,敬请期待!!!
转载自:https://juejin.cn/post/7130275578422231070