Как сделать декоратор, который обрабатывает аварийное переключение с помощью двигателя и торнадо?
Я пытаюсь написать декоратор, который принимает функцию, которая взаимодействует с 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
Но делайте это только для идемпотентных операций!