Нестабильные внешние сервисы: способ решения проблемы

в 9:39, , рубрики: decorator, python, декораторы, Питон

Есть у нас пара внешних HTTP сервисов, которые работают довольно нестабильно. Да, мы должны были об этом подумать на этапе разработки, но мы все время откладывали в длинный ящик, аккуратно писали logging.error и двигались по другим направлениям.


Но, как обычно бывает, столкнулись с реальностью — сервисы работают действительно нестабильно. Иногда медленно, иногда возвращают 404 или 500, иногда вылетают с socket timeout. Сотни исключений сыпались на почту ежедневно.

С этим надо было что-то делать. Так родился safe_exec декоратор. Декорируя функцию с его помощью, нужно указать ожидаемые исключения и поведение если исключение будет генерироваться. Например, повторить 3 раза через 1 секунду, если исключение все еще генерируется — вернуть значение по умолчанию.

Декоратор:

import logging
import time

__all__ = ("safe_exec",)


def safe_exec(exceptions, shakes=3, timeout=1, title="", default=None):
    """
    Decorator to safely execute function or method
    within `shakes` trying
    """
    def wrap(func):
        if not isinstance(exceptions, tuple):
            raise TypeError(
                "First argument of safe_exec should be tuple of exceptions"
            )

        def wrapped(*args, **kwargs):
            name = func.__name__
            result = None
            for shake in range(shakes):
                try:
                    result = func(*args, **kwargs)
                    break
                except exceptions:
                    logging.warn("%s: Sorry, can't execute %s, shake #%d",
                        title,
                        name,
                        shake,
                        exc_info=True
                    )
                    time.sleep(timeout)
            else:
                logging.error(
                    "%s: Can't execute `%s` after %d shakes",
                    title,
                    name,
                    shakes
                )
                return default

            return result
        return wrapped
    return wrap

Пример использования:

import urllib2

@safe_exec((urllib2.URLError, urllib2.HTTPError), shakes=2)
def download(url):
    return urllib2.urlopen(url).read()

download("http://slow-resource.com/")

Автор: joymax

* - обязательные к заполнению поля