跳转到主要内容

退避和重试的函数装饰

项目描述

backoff

https://travis-ci.org/litl/backoff.svg https://coveralls.io/repos/litl/backoff/badge.svg https://github.com/litl/backoff/workflows/CodeQL/badge.svg https://img.shields.io/pypi/v/backoff.svg https://img.shields.io/github/license/litl/backoff

退避和重试的函数装饰

此模块提供函数装饰器,可用于包装函数以便在满足某些条件之前进行重试。它旨在在访问不可靠资源且可能存在间歇性失败的情况下使用,例如网络资源和外部API。更普遍地说,它也可能用于动态轮询资源以获取外部生成的内容。

装饰器支持同步代码的常规函数和asyncio的协程。

示例

由于Kenneth Reitz的requests模块已成为Python中同步HTTP客户端的事实标准,以下网络示例使用它编写,但backoff模块并不要求使用它。

@backoff.on_exception

当抛出指定异常时,使用on_exception装饰器进行重试。以下是一个在抛出任何requests异常时使用指数退避的示例

@backoff.on_exception(backoff.expo,
                      requests.exceptions.RequestException)
def get_url(url):
    return requests.get(url)

装饰器还可以接受一个异常元组,用于在需要为多个异常类型实现相同的退避行为时。

@backoff.on_exception(backoff.expo,
                      (requests.exceptions.Timeout,
                       requests.exceptions.ConnectionError))
def get_url(url):
    return requests.get(url)

放弃条件

可选的关键字参数可以指定放弃的条件。

关键字参数 max_time 指定在放弃之前可以经过的最大总时间(以秒为单位)。

@backoff.on_exception(backoff.expo,
                      requests.exceptions.RequestException,
                      max_time=60)
def get_url(url):
    return requests.get(url)

关键字参数 max_tries 指定在放弃之前对目标函数进行调用的最大次数。

@backoff.on_exception(backoff.expo,
                      requests.exceptions.RequestException,
                      max_tries=8,
                      jitter=None)
def get_url(url):
    return requests.get(url)

在某些情况下,可能需要检查引发的异常实例本身以确定它是否是可重试的条件。可以使用 giveup 关键字参数指定一个函数,该函数接受异常并返回一个真值,如果异常不应该重试。

def fatal_code(e):
    return 400 <= e.response.status_code < 500

@backoff.on_exception(backoff.expo,
                      requests.exceptions.RequestException,
                      max_time=300,
                      giveup=fatal_code)
def get_url(url):
    return requests.get(url)

默认情况下,当发生放弃事件时,相关的异常将被重新抛出,因此调用 on_exception 装饰的函数的代码可能仍然需要进行异常处理。可以使用 raise_on_giveup 关键字参数选择性地禁用此行为。

在下面的代码中,当发生放弃时,将不会引发 requests.exceptions.RequestException。请注意,在这种情况下,装饰的函数将返回 None,而不管 on_exception 处理器中的逻辑如何。

def fatal_code(e):
    return 400 <= e.response.status_code < 500

@backoff.on_exception(backoff.expo,
                      requests.exceptions.RequestException,
                      max_time=300,
                      raise_on_giveup=False,
                      giveup=fatal_code)
def get_url(url):
    return requests.get(url)

这对于非关键代码很有用,尽管你仍然希望在 backoff.on_exception 内部重试代码,但即使所有重试都失败,也希望继续执行。

@backoff.on_predicate

on_predicate 装饰器用于在目标函数的返回值满足特定条件时进行重试。当需要轮询外部生成内容时,这可能很有用。

以下是一个示例,当目标函数的返回值为空列表时,使用斐波那契数列退避。

@backoff.on_predicate(backoff.fibo, lambda x: x == [], max_value=13)
def poll_for_messages(queue):
    return queue.get()

初始化等待生成器时传递了额外的关键字参数,因此上面的 max_value 参数在初始化 fibo 生成器时作为关键字参数传递。

如果没有指定,默认的谓词参数为假值测试,因此上述内容可以更简洁地写为

@backoff.on_predicate(backoff.fibo, max_value=13)
def poll_for_message(queue):
    return queue.get()

更简单地说,可以定义一个函数,它每秒继续轮询,直到获得一个非假值的结果,如下所示

@backoff.on_predicate(backoff.constant, jitter=None, interval=1)
def poll_for_message(queue):
    return queue.get()

禁用抖动以保持轮询频率固定。

@backoff.runtime

您还可以使用 backoff.runtime 生成器来利用装饰方法返回的值或抛出的异常。

例如,使用响应的 Retry-After 标头中的值

@backoff.on_predicate(
    backoff.runtime,
    predicate=lambda r: r.status_code == 429,
    value=lambda r: int(r.headers.get("Retry-After")),
    jitter=None,
)
def get_url():
    return requests.get(url)

抖动

可以通过将抖动算法作为 jitter 关键字参数提供给任一退避装饰器。此参数应是一个接受原始未处理的退避值并返回其抖动版本的函数。

从版本 1.2 开始,默认抖动函数 backoff.full_jitter 实现了 AWS 架构博客中定义的“完全抖动”算法,见 指数退避和抖动 文章。请注意,使用此算法时,等待生成器产生的时间实际上是等待的最大时间。

backoff 的早期版本默认将一些随机毫秒数(最多 1 秒)添加到原始睡眠值。如果需要,此行为现在作为 backoff.random_jitter 可用。

使用多个装饰器

退避装饰器也可以组合起来,为不同的情况指定不同的退避行为。

@backoff.on_predicate(backoff.fibo, max_value=13)
@backoff.on_exception(backoff.expo,
                      requests.exceptions.HTTPError,
                      max_time=60)
@backoff.on_exception(backoff.expo,
                      requests.exceptions.Timeout,
                      max_time=300)
def poll_for_message(queue):
    return queue.get()

运行时配置

装饰器函数 on_exceptionon_predicate 通常在导入时进行评估。当关键字参数作为常量值传递时,这是可以的,但是假设我们想在运行时查询配置选项的字典,这些选项在运行时才可用。相关值在导入时不可用。相反,装饰器函数可以传递可调用对象,这些对象在运行时评估以获取值。

def lookup_max_time():
    # pretend we have a global reference to 'app' here
    # and that it has a dictionary-like 'config' property
    return app.config["BACKOFF_MAX_TIME"]

@backoff.on_exception(backoff.expo,
                      ValueError,
                      max_time=lookup_max_time)

事件处理器

两个退避装饰器都可选地接受事件处理器函数,使用关键字参数 on_successon_backoffon_giveup。这可能有助于报告统计信息或执行其他自定义日志记录。

处理器必须是接受字典参数的可调用对象,该字典包含调用的详细信息。有效的键包括

  • target:被调用函数或方法的引用

  • args:传递给函数的位置参数

  • kwargs:传递给函数的关键字参数

  • tries:迄今为止的调用尝试次数

  • elapsed:迄今为止经过的时间(以秒为单位)

  • wait:要等待的秒数(仅 on_backoff 处理器)

  • value:触发退避的值(仅 on_predicate 装饰器)

可以实现一个打印退避事件详细信息的处理器,如下所示

def backoff_hdlr(details):
    print ("Backing off {wait:0.1f} seconds after {tries} tries "
           "calling function {target} with args {args} and kwargs "
           "{kwargs}".format(**details))

@backoff.on_exception(backoff.expo,
                      requests.exceptions.RequestException,
                      on_backoff=backoff_hdlr)
def get_url(url):
    return requests.get(url)

每个事件类型多个处理器

在所有情况下,也接受处理器函数的迭代器,按顺序调用。例如,您可能将简单的处理器函数列表作为 on_backoff 关键字参数的值提供。

@backoff.on_exception(backoff.expo,
                      requests.exceptions.RequestException,
                      on_backoff=[backoff_hdlr1, backoff_hdlr2])
def get_url(url):
    return requests.get(url)

获取异常信息

对于 on_exception 装饰器,所有 on_backoffon_giveup 处理器都在处理异常的 except 块内部被调用。因此,异常信息可以通过 Python 标准库中的 sys.exc_info()traceback 模块提供给处理器函数。异常也存在于传递给处理器的 details 字典中的 exception 键中。

异步代码

Backoff 支持在 Python 3.5 及以上版本中执行异步操作。

要在基于 asyncio 的异步代码中使用 backoff,只需将 backoff.on_exceptionbackoff.on_predicate 应用于协程。您还可以使用协程作为 on_successon_backoffon_giveup 事件处理器的函数,其他接口保持不变。

以下示例使用 aiohttp 异步 HTTP 客户端/服务器库。

@backoff.on_exception(backoff.expo, aiohttp.ClientError, max_time=60)
async def get_url(url):
    async with aiohttp.ClientSession(raise_for_status=True) as session:
        async with session.get(url) as response:
            return await response.text()

日志配置

默认情况下,退避和重试尝试被记录到“backoff”记录器。默认情况下,该记录器配置为 NullHandler,因此除非您配置处理器,否则将没有输出。程序化地,这可能可以通过以下简单方法实现:

logging.getLogger('backoff').addHandler(logging.StreamHandler())

默认日志级别是 INFO,这意味着在任何重试事件发生时都会记录日志。如果您只想在放弃事件发生时记录日志,请将记录器级别设置为 ERROR。

logging.getLogger('backoff').setLevel(logging.ERROR)

您还可以使用 logger 关键字参数指定一个不同的记录器。如果指定了一个字符串值,则将通过名称查找记录器。

@backoff.on_exception(backoff.expo,
                      requests.exceptions.RequestException,
                      logger='my_logger')
# ...

也可以直接指定 Logger(或 LoggerAdapter)对象。

my_logger = logging.getLogger('my_logger')
my_handler = logging.StreamHandler()
my_logger.addHandler(my_handler)
my_logger.setLevel(logging.ERROR)

@backoff.on_exception(backoff.expo,
                      requests.exceptions.RequestException,
                      logger=my_logger)
# ...

可以通过指定 logger=None 完全禁用默认日志记录。在这种情况下,如果需要,可以通过使用自定义事件处理器定义替代的日志记录行为。

项目详情


下载文件

下载适用于您平台的文件。如果您不确定选择哪个,请了解更多关于安装包的信息。

源分发

backoff-2.2.1.tar.gz (17.0 kB 查看哈希值)

上传时间

构建分发

backoff-2.2.1-py3-none-any.whl (15.1 kB 查看哈希值)

上传时间 Python 3

支持

AWS AWS 云计算和安全赞助商 Datadog Datadog 监控 Fastly Fastly CDN Google Google 下载分析 Microsoft Microsoft PSF赞助商 Pingdom Pingdom 监控 Sentry Sentry 错误日志 StatusPage StatusPage 状态页面