Как сделать декоратор, который обрабатывает аварийное переключение с помощью двигателя и торнадо?

Я пытаюсь написать декоратор, который принимает функцию, которая взаимодействует с 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 decorator?

редактировать: сейчас нет декораторов. Это должно надежно создать счетчик и вернуть 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']})

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

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

Другие вопросы по тегам