Вопрос по python, transactions – App Engine, транзакции и идемпотентность

6

Пожалуйста, помогите мне найти мое недоразумение.

Я пишу RPG на App Engine. Определенные действия, предпринимаемые игроком, потребляют определенный стат. Если показатель достигает нуля, игрок больше не может предпринимать никаких действий. Однако я начал беспокоиться о мошенничестве с игроками - что, если игрок отправил два действия очень быстро, прямо рядом друг с другом? Если код, который уменьшает статистику, отсутствует в транзакции, то у игрока есть шанс выполнить действие дважды. Итак, я должен обернуть код, который уменьшает статистику в транзакции, верно? Все идет нормально.

В GAE Python, у нас есть это вдокументация:

Note: If your app receives an exception when submitting a transaction, it does not always mean that the transaction failed. You can receive Timeout, TransactionFailedError, or InternalError exceptions in cases where transactions have been committed and eventually will be applied successfully. Whenever possible, make your Datastore transactions idempotent so that if you repeat a transaction, the end result will be the same.

Упс. Это означает, что функция, которую я выполнял, выглядит так:

<code>
def decrement(player_key, value=5):
  player = Player.get(player_key)
  player.stat -= value
  player.put()
</code>

Ну, это не сработает, потому что это не идемпотент, верно? Если я обведу цикл повторения (мне нужно в Python? Я прочитал, что мне не нужно на SO ... но я не могу найти его в документации), это может увеличить значение в два раза, право? Поскольку мой код может перехватить исключение, но хранилище данных все еще фиксирует данные ... а? Как это исправить? Это тот случай, когда мне нужнораспределенные транзакции? Действительно ли я?

@TravisWebb Не согласен. Транзакционная безопасность не является «преждевременной оптимизацией», а коллизии транзакций особенно маловероятны. Nick Johnson
@TravisWebb Смотрите мой ответ для "кошмара" подробности. Nick Johnson
Ваш шаблон находится на правильном пути, но GAE имеет довольно много неприятных нюансов, которые затрудняют хирургически точную реализацию, такую как эта. По моему опыту работы с GAE иногда оно стоит усилий, а иногда нет. Travis Webb
Ну, да, и это хороший момент ... но прежде чем я засорю свой код кучей трудно диагностируемых, трудно воспроизводимых ошибок, я бы хотел узнать, какой шаблон мне здесь нужен. D. Hayes

Ваш Ответ

4   ответа
1

что вы описываете, это может и не быть проблемой. Подумайте об этом таким образом:

У вашего игрока остался один стат. Затем он злонамеренно посылает 2 действия (А1 и А2) мгновенно, каждое из которых должно потреблять эту точку. И А1, и А2 являются транзакционными.

Вот что может произойти:

А1 успешно. А2 затем прервется. Все хорошо.

А1 не работает законно (без изменения данных). Повторить запланировано. А2 тогда пытается, преуспевает. Когда A1 попытается снова, он будет прерван.

A1 успешно, но сообщает об ошибке. Повторить запланировано. В следующий раз, когда попытается А1 или А2, они прервутся.

Чтобы это работало, вам нужно следить за тем, завершили ли A1 и A2 - может быть, дать им UUID задачи и сохранить список выполненных задач? Или даже просто использовать очередь задач.

Error: User Rate Limit Exceeded D. Hayes
13

ответ Ника неправильный. Транзакция DHayes не является идемпотентной, поэтому, если она запускается несколько раз (то есть повторяется попытка, когда первая попытка не удалась, когда она не выполнялась), тогда значение будет уменьшено в несколько раз. Ник говорит, что «хранилище данных проверяет, были ли объекты изменены с момента их выборки», но это не предотвращает проблему, поскольку две транзакции имели отдельные выборки, а вторая выборка была ПОСЛЕ первой транзакции завершена.

Чтобы решить проблему, вы можете сделать транзакцию идемпотентной, создав «Ключ транзакции». и запись этого ключа в новый объект как часть транзакции. Вторая транзакция может проверить этот ключ транзакции и, если она найдена, ничего не сделает. Ключ транзакции может быть удален, как только вы убедитесь, что транзакция завершена, или вы прекратили повторную попытку.

Я хотел бы знать, что является "чрезвычайно редким" означает для AppEngine (1 на миллион или 1 на миллиард?), но я советую, что идемпотентные транзакции необходимы для финансовых вопросов, но не для игровых очков или даже "жизней" ;-)

1

что гораздо быстрее, чем в хранилище данных (что вам понадобится, если эта статистика часто используется в вашем приложении). Memcache предоставляет вам хорошую функцию:decr который:

Atomically decrements a key's value. Internally, the value is a unsigned 64-bit integer. Memcache doesn't check 64-bit overflows. The value, if too large, will wrap around.

Ищиdecr Вот, Затем следует использовать задачу для сохранения значения этого ключа в хранилище данных либо каждые x секунд, либо при выполнении определенного условия.

Error: User Rate Limit Exceeded D. Hayes
Error: User Rate Limit Exceeded[1].
4

Edit: This is incorrect - please see the comments.

Ваш код в порядке. Идемпотентность, на которую ссылаются документы, касается побочных эффектов. Как объясняют документы, ваша транзакционная функция может выполняться несколько раз; в таких ситуациях, если у функции есть побочные эффекты, они будут применяться несколько раз. Поскольку ваша транзакционная функция этого не делает, все будет в порядке.

Примером проблемной функции относительно идемпотентности может быть что-то вроде этого:

def do_something(self):
  def _tx():
    # Do something transactional
    self.counter += 1
  db.run_in_transaction(_tx)

В этом случае,self.counter может быть увеличен на 1 или потенциально больше 1. Этого можно избежать, выполнив побочные эффекты вне транзакции:

def do_something(self):
  def _tx():
    # Do something transactional
    return 1
  self.counter += db.run_in_transaction(_tx)
Error: User Rate Limit Exceeded D. Hayes
Error: User Rate Limit ExceededdecrementError: User Rate Limit Exceeded D. Hayes
Error: User Rate Limit Exceeded
Error: User Rate Limit Exceeded
Error: User Rate Limit Exceeded

Похожие вопросы