【PY模块】Tenacity 重试

说明

官方文档:https://tenacity.readthedocs.io/en/latest/

Tenacity 是一个 Apache 2.0 许可的通用重试库,用 Python 编写,用于简化将重试行为添加到几乎任何内容的任务。它起源于 retrying的一个分支,遗憾的是retrying不再 维护。Tenacity 与retrying不兼容,但添加了重要的新功能并修复了许多长期存在的错误。

特征

  • 通用装饰器 API
  • 指定停止条件(即限制尝试次数)
  • 指定等待条件(即尝试之间的指数退避休眠)
  • 自定义对预期返回结果的重试
  • 停止等待重试条件 任意组合
  • 协程 重试
  • 使用上下文管理器重试代码块

安装: pip install tenacity

例子

不加任何条件,一直重试正确结果,才停止重试

import random
from tenacity import retry

@retry
def fun():
    r = random.randint(1, 3)
    print(f'当前 {r}')
    assert r == 2

if __name__ == '__main__':
    fun()

-----------------------------------
当前 1
当前 3
当前 3
当前 2

Process finished with exit code 0

停止条件

stop_after_attempt 比较常用,停止条件还有很多

from tenacity import retry
from tenacity.stop import stop_after_attempt

# 重试 2 次,停止
@retry(stop=stop_after_attempt(2))
def fun():
    pass

等待条件

from tenacity import retry

# 每次重试等待 2秒 
from tenacity.wait import wait_fixed
@retry(wait=wait_fixed(2))
def fun():

# 每次重试 随机等待 1~2秒
from tenacity.wait import wait_random
@retry(wait=wait_random(min=1, max=2))
def fun():

# 在重试分布式服务和其他远程端点时,结合固定等待和抖动(以帮助避免雷鸣般的群体)
# 固定基础等待 3 秒,随机增加 0~2 秒
from tenacity.wait import wait_fixed, wait_random
@retry(wait=wait_fixed(3) + wait_random(0, 2))
def fun():

# 生成一个等待链(等待时间的列表)等待时间相对于:[3,3,3,7,7,9]
from tenacity.wait import wait_chain, wait_fixed
@retry(wait=wait_chain(*[wait_fixed(3) for i in range(3)] +
                        [wait_fixed(7) for i in range(2)] +
                        [wait_fixed(9)]))
def fun():

wait_exponential 重试分布式服务和其他远程端点时,等待时间呈指数增长

  • multiplier: 增长的指数【默认值 1
  • min: 最小等待时间 【默认值 0
  • max: 最大等待时间【默认值 sys.maxsize / 2
import random
from loguru import logger
from tenacity import retry
from tenacity.wait import wait_exponential

@retry(wait=wait_exponential(multiplier=1, min=4, max=10))
def fun():
    r = random.randint(1, 5)
    logger.info(f'当前 {r}')
    assert r == 6

if __name__ == '__main__':
    fun()

-----------------------------------------------
2022-02-28 10:52:37.918 | INFO     | __main__:fun:10 - 当前 2
2022-02-28 10:52:41.930 | INFO     | __main__:fun:10 - 当前 2
2022-02-28 10:52:45.938 | INFO     | __main__:fun:10 - 当前 1
2022-02-28 10:52:49.949 | INFO     | __main__:fun:10 - 当前 4
2022-02-28 10:52:57.952 | INFO     | __main__:fun:10 - 当前 3
2022-02-28 10:53:07.965 | INFO     | __main__:fun:10 - 当前 1
2022-02-28 10:53:17.975 | INFO     | __main__:fun:10 - 当前 4
2022-02-28 10:53:27.977 | INFO     | __main__:fun:10 - 当前 3

stop_after_delay 函数运行超过限制秒数,引发重试异常

import random
import time
from tenacity import retry
from tenacity.wait import stop_after_delay
@retry(stop=stop_after_delay(2))
def fun():
    r = random.randint(1, 5)
    print(f'当前 {r}')
    time.sleep(2)
    assert r == 2
    print(f'对了')

-----------------------------------
tenacity.RetryError: RetryError[<Future at 0x260529a62b0 state=finished raised AssertionError>]

是否重试

# AssertionError 异常,触发重试
from tenacity.retry import retry_if_exception_type
@retry(retry=retry_if_exception_type(AssertionError))
def fun():

使用函数判断,是否重试

import random
from loguru import logger
from tenacity import retry
from tenacity.retry import retry_if_result

@retry(retry=retry_if_result(lambda x: x != 2))
def fun():
    r = random.randint(1, 5)
    logger.info(f'当前 {r}')
    return r

if __name__ == '__main__':
    fun()
--------------------------------------------
2022-02-28 11:26:33.588 | INFO     | __main__:fun:10 - 当前 5
2022-02-28 11:26:33.588 | INFO     | __main__:fun:10 - 当前 1
2022-02-28 11:26:33.588 | INFO     | __main__:fun:10 - 当前 3
2022-02-28 11:26:33.588 | INFO     | __main__:fun:10 - 当前 2

组合 重试条件

import random
from loguru import logger
from tenacity import retry
from tenacity.retry import retry_if_result, retry_if_exception_type

"""
函数结果不可能为 2 和 3
- 2 重试
- 3 触发 AssertionError 异常,重试
"""

@retry(retry=(retry_if_result(lambda x: x != 2) and retry_if_exception_type(AssertionError)))
def fun():
    r = random.randint(1, 5)
    logger.info(f'当前 {r}')
    if r == 3:
        raise AssertionError(f'r is 3')
    return r

if __name__ == '__main__':
    logger.info(f'fun {fun()}')

协程重试

import random
import asyncio
from loguru import logger
from tenacity import retry
from tenacity.wait import wait_fixed

@retry(wait=wait_fixed(2))
async def fun():
    r = random.randint(1, 3)
    logger.info(f'当前 {r}')
    assert r == 2

if __name__ == '__main__':
    asyncio.run(fun())
-----------------------------------------
2022-02-28 12:36:07.801 | INFO     | __main__:fun:11 - 当前 3
2022-02-28 12:36:09.805 | INFO     | __main__:fun:11 - 当前 1
2022-02-28 12:36:11.807 | INFO     | __main__:fun:11 - 当前 3
2022-02-28 12:36:13.814 | INFO     | __main__:fun:11 - 当前 2

统计数据

函数.retry.statistics

  • 重试次数
  • 开始运行时间
  • 整个重试的耗时
import random
from loguru import logger
from tenacity import retry
from tenacity.retry import retry_if_result

@retry(reraise=True, retry=retry_if_result(lambda x: x != 2))
def fun():
    r = random.randint(1, 5)
    logger.info(f'当前 {r}')
    return r

if __name__ == '__main__':
    logger.info(f'fun {fun()}')
    logger.info(f'fun {fun.retry.statistics}')
----------------------------------------------------
2022-02-28 11:43:33.558 | INFO     | __main__:fun:10 - 当前 1
2022-02-28 11:43:33.558 | INFO     | __main__:fun:10 - 当前 1
2022-02-28 11:43:33.558 | INFO     | __main__:fun:10 - 当前 1
2022-02-28 11:43:33.559 | INFO     | __main__:fun:10 - 当前 3
2022-02-28 11:43:33.559 | INFO     | __main__:fun:10 - 当前 5
2022-02-28 11:43:33.559 | INFO     | __main__:fun:10 - 当前 4
2022-02-28 11:43:33.559 | INFO     | __main__:fun:10 - 当前 2
2022-02-28 11:43:33.559 | INFO     | __main__:<module>:15 - fun 2
2022-02-28 11:43:33.559 | INFO     | __main__:<module>:16 - fun {'start_time': 6972.546, 'attempt_number': 7, 'idle_for': 0, 'delay_since_first_attempt': 0.0}

重试代码块

重试代码块的逻辑包含的一个 for 循环中,然后包含一个 上下文管理器,使需要重试的逻辑脱离函数

import random
from loguru import logger
from tenacity import Retrying, RetryError, stop_after_attempt

try:
    # 重试 3 次
    for attempt in Retrying(stop=stop_after_attempt(3)):  # 重试、等待、停止 条件设置
        with attempt:  # 重试逻辑 包含在 上下文管理器
            r = random.randint(1, 3)
            logger.info(f'r -> {r}')
            assert r == 2
except RetryError:
    logger.error(f'超出重试次数')  # 超出重试次数处理逻辑
    pass
----------------------------------------------------------
2022-02-28 13:08:13.491 | INFO     | __main__:<module>:10 - r -> 3
2022-02-28 13:08:13.492 | INFO     | __main__:<module>:10 - r -> 3
2022-02-28 13:08:13.492 | INFO     | __main__:<module>:10 - r -> 3
2022-02-28 13:08:13.492 | ERROR    | __main__:<module>:13 - 超出重试次数

异步重试代码块

异步使用

import random
import asyncio
from loguru import logger
from tenacity import AsyncRetrying, RetryError, stop_after_attempt

async def fun():
    try:
        async for attempt in AsyncRetrying(stop=stop_after_attempt(3)):  # 重试、等待、停止 条件设置
            # 这个 with 前不能带 async
            with attempt:  # 重试逻辑 包含在 上下文管理器
                r = random.randint(1, 3)
                logger.debug(f'r -> {r}')
                assert r == 2
    except RetryError:
        logger.error('达到重试上限')
        pass

if __name__ == '__main__':
    asyncio.run(fun())
------------------------------------------------------------------
2022-02-28 13:09:17.247 | DEBUG    | __main__:fun:13 - r -> 3
2022-02-28 13:09:17.247 | DEBUG    | __main__:fun:13 - r -> 1
2022-02-28 13:09:17.247 | DEBUG    | __main__:fun:13 - r -> 3
2022-02-28 13:09:17.247 | ERROR    | __main__:fun:16 - 达到重试上限
发表评论 / Comment

用心评论~