likes
comments
collection
share

你也可以手敲一个高速下载器(二)重试

作者站长头像
站长
· 阅读数 22

你也可以手敲一个高速下载器(二)重试

前言

上节说过,高速下载器采用的多任务分段下载策略,所以要保证每个任务都能成功,所以就要有异常的重试。正常我们的操作是设置一个重试最大次数,然后发生异常的时候重新调用使用的方法,次数加一,直到成功或者达到最大次数。那么我们采用另一种做法:使用一个第三方的库: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次重试全部失败其实是打印重复了,我们要的是在第五次的时候直接输出全部失败就可以了,并且我们是不知道异常的信息的,这时可以把afterretry_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,当然,这个库的内容肯定不止我说的这么一点,更详细的大家一个去看官方的文档。我们下节继续,敬请期待!!!