Как сделать декоратор, который обрабатывает отказоустойчивость с мотором и торнадо?

Я пытаюсь написать декоратор, который принимает функцию, которая взаимодействует с mongodb, и если возникает исключение, он повторяет взаимодействие. У меня есть следующий код:

def handle_failover(f):
    def wrapper(*args):
        for i in range(40):
            try:
                yield f(*args)
                break
            except pymongo.errors.AutoReconnect:
                loop = IOLoop.instance()
                yield gen.Task(loop.add_timeout, time.time() + 0.25)
    return wrapper


class CreateHandler(DatabaseHandler):
    @handle_failover
    def create_counter(self, collection):
        object_id = yield self.db[collection].insert({'n': 0})
        return object_id

    @gen.coroutine
    def post(self, collection):
        object_id = yield self.create_counter(collection)
        self.finish({'id': object_id})

Но это не работает. Выдает ошибку, что create_counter выдает генератор. Я пытался сделать все функции @gen.coroutines, и это не помогло.

Как я могу заставить работать декоратор handle_failover?

редактировать: пока нет декораторов. Это должно надежно создать счетчик и вернуть object_id пользователю. Если возникает исключение, отображается 500 страниц.

class CreateHandler(DatabaseHandler):
    @gen.coroutine
    def create_counter(self, collection, data):
        for i in range(FAILOVER_TRIES):
            try:
                yield self.db[collection].insert(data)
                break
            except pymongo.errors.AutoReconnect:
                loop = IOLoop.instance()
                yield gen.Task(loop.add_timeout, time.time() + FAILOVER_SLEEP)
            except pymongo.errors.DuplicateKeyError:
                break
        else:
            raise Exception("Can't create new counter.")

    @gen.coroutine
    def post(self, collection):
        object_id = bson.objectid.ObjectId()
        data = {
            '_id': object_id,
            'n': 0
        }
        yield self.create_counter(collection, data)
        self.set_status(201)
        self.set_header('Location', '/%s/%s' % (collection, str(object_id)))
        self.finish({})

Хотя я до сих пор не знаю, как сделать приращение счетчика идемпотентным, потому что трюк с DuplicateKeyError здесь неприменим:

class CounterHandler(CounterIDHandler):
    def increment(self, collection, object_id, n):
        result = yield self.db[collection].update({'_id': object_id}, {'$inc': {'n': int(n)}})
        return result

    @gen.coroutine
    def post(self, collection, counter_id, n):
        object_id = self.get_object_id(counter_id)
        if not n or not int(n):
            n = 1
        result = yield self.increment(collection, object_id, n)
        self.finish({'resp': result['updatedExisting']})

person ragezor    schedule 19.04.2014    source источник


Ответы (1)


Вы, скорее всего, не хотите этого делать. Лучше показать пользователю ошибку, чем повторять операцию.

Слепая повторная попытка любой вставки, которая вызывает AutoReconnect, — плохая идея, потому что вы не знаете, выполнила ли MongoDB вставку до того, как вы потеряли подключение или нет. В этом случае вы не знаете, получится ли у вас одна или две записи с {'n': 0}. Таким образом, вы должны убедиться, что любая операция, которую вы пытаетесь повторить таким образом, является идемпотентной. Подробнее см. в моей статье "спасите обезьяну". Информация.

Если вы определенно хотите сделать такую ​​оболочку, вам нужно убедиться, что f и wrapper являются сопрограммами. Кроме того, если f выдает ошибку 40 раз, вы должны повторно вызвать последнюю ошибку. Если f завершается успешно, вы должны вернуть возвращаемое значение:

def handle_failover(f):
    @gen.coroutine
    def wrapper(*args):
        retries = 40
        i = 0
        while True:
            try:
                ret = yield gen.coroutine(f)(*args)
                raise gen.Return(ret)
            except pymongo.errors.AutoReconnect:
                if i < retries:
                    i += 1
                    loop = IOLoop.instance()
                    yield gen.Task(loop.add_timeout, time.time() + 0.25)
                else:
                    raise
    return wrapper

Но делайте это только для идемпотентных операций!

person A. Jesse Jiryu Davis    schedule 19.04.2014
comment
Хорошо, я прочитал Спасите обезьяну. Таким образом, нет надежного способа написать такой запрос: self.db[collection].update({'_id': object_id}, {'$inc': {'n': int(n)}})? Трюк с дубликатом ключа здесь не работает. Также почему значение возвращается через gen.Return()? Нормальная отдача не годится? Я использую python3.4, поэтому я могу смешивать возвраты и доходности в генераторах. - person ragezor; 20.04.2014
comment
Я отредактировал свой вопрос с новым кодом. Я думаю, что в последнем примере Save the Monkey, когда за исключением DuplicateKeyError, он должен выйти из цикла for и не пройти. - person ragezor; 20.04.2014
comment
Ах, в Python 3.3 и 3.4 возврат работает нормально. Нужен только gen.Return в старых Python. И вы правы, в тех редких случаях, когда $inc приводит к AutoReconnect, невозможно узнать, было ли увеличено значение или нет. Кстати, то же самое верно и для систем SQL: если вы совершаете транзакцию и получаете сетевую ошибку, вы не знаете, зафиксировал ли сервер или откатился. Это просто жизнь в распределенных системах. - person A. Jesse Jiryu Davis; 20.04.2014