2012-04-15 9 views
6

Por favor, ayúdame a encontrar mi malentendido.App Engine, transacciones e idempotencia

Estoy escribiendo un juego de rol en App Engine. Ciertas acciones que el jugador toma consumen una determinada estadística. Si la estadística llega a cero, el jugador no puede tomar más acciones. Sin embargo, comencé a preocuparme por engañar a los jugadores: ¿qué pasaría si un jugador enviara dos acciones muy rápido, uno al lado del otro? Si el código que disminuye la estadística no está en una transacción, entonces el jugador tiene la posibilidad de realizar la acción dos veces. Entonces, debo ajustar el código que reduce la estadística en una transacción, ¿verdad? Hasta aquí todo bien.

En GAE Python, sin embargo, tenemos esto en el documentation:

Nota: Si su aplicación recibe una excepción cuando la presentación de una transacción, no siempre significa que la transacción ha fallado. Puede recibir excepciones Timeout, TransactionFailedError o InternalError en los casos en que se hayan confirmado transacciones y, finalmente, se aplicará . Siempre que sea posible, defina las transacciones de su Almacén de datos de manera que que si repite una transacción, el resultado final será el mismo.

Whoops. Eso significa que la función estaba corriendo que tiene este aspecto:


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

Bueno, eso no va a funcionar porque la cosa no es idempotente, ¿verdad? Si pongo un ciclo de reintento alrededor (¿tengo que hacerlo en Python? He leído que no necesito hacerlo en SO ... pero no puedo encontrarlo en los documentos) podría incrementar el valor dos veces, ¿derecho? Dado que mi código puede detectar una excepción, pero el almacén de datos aún cometió los datos ... ¿eh? ¿Cómo puedo solucionar esto? ¿Es este un caso en el que necesito distributed transactions? ¿De verdad?

+1

Bueno, sí, y es un buen punto ... pero antes de escribir mi código con un montón de difícil de diagnosticar, difícil de - reproducir errores Me gustaría aprender qué patrón debería seguir aquí. –

+0

Su patrón está en el camino correcto, pero GAE tiene bastantes matices frustrantes que dificultan la implementación quirúrgica precisa como esta. En mi experiencia con el GAE, a veces vale la pena el esfuerzo, y a veces no. –

+1

@TravisWebb No estoy de acuerdo. La seguridad transaccional no es una "optimización prematura", ni las colisiones de transacción son particularmente improbables. –

Respuesta

13

Primero, la respuesta de Nick es incorrecta. La transacción de DHayes no es idempotente, por lo que si se ejecuta varias veces (es decir, un reintento cuando se pensó que el primer intento había fallado, cuando no lo hizo), el valor se habrá reducido varias veces. Nick dice que "el almacén de datos comprueba si las entidades se han modificado desde que se obtuvieron", pero eso no evita el problema ya que las dos transacciones tenían recuperaciones separadas, y la segunda recuperación fue DESPUÉS de que se complete la primera transacción.

Para resolver el problema, puede hacer que la transacción sea idempotente creando una "clave de transacción" y registrando esa clave en una nueva entidad como parte de la transacción. La segunda transacción puede verificar esa clave de transacción, y si se encuentra, no hará nada. La clave de transacción puede eliminarse una vez que esté satisfecho de que la transacción se completó o abandona el reintentar.

Me gustaría saber qué significa "extremadamente raro" para App Engine (1-en-un-millón o 1-en-un-billón?), Pero mi consejo es que las transacciones idempotentes son necesarias para asuntos financieros , pero no para puntajes de juegos, o incluso "vidas" ;-)

1

¿No debería tratar de almacenar este tipo de información en Memcache, que es mucho más rápido que el Datastore (algo que necesitará si esta estadística se utiliza con frecuencia en su aplicación). Memcache le proporciona una buena función: decr que:

Disminuye automáticamente el valor de una clave. Internamente, el valor es un entero sin signo de 64 bits. Memcache no comprueba los desbordamientos de 64 bits. El valor, si es demasiado grande, se ajustará.

Buscar decrhere. A continuación, debe usar una tarea para guardar el valor de esta clave en el almacén de datos, ya sea cada x segundos o cuando se cumple una determinada condición.

+0

Gracias por la respuesta, pero no creo que eso funcione. Si esto fuera una especie de contador global que pudiera pagar por valores difusos sería perfecto, pero me imagino que los jugadores se enojarían mucho si el valor se estropeara debido a un desalojo de Memcache. –

+0

Tenga en cuenta que la función Memcache decr() también "limita la reducción por debajo de cero a cero" [\ [1 \]] (https://cloud.google.com/appengine/docs/python/refdocs/google.appengine.api. memcache # google.appengine.api.memcache.Client.decr). – Lee

1

Si piensa cuidadosamente sobre lo que está describiendo, puede que en realidad no sea un problema. Piénselo de esta manera:

Su jugador tiene un punto stat stat. A continuación, maliciosamente envía 2 acciones (A1 y A2) instantáneamente que cada uno necesita consumir ese punto. Tanto A1 como A2 son transaccionales.

Esto es lo que podría suceder:

A1 tiene éxito. A2 abortará. Todo bien.

A1 falla legítimamente (sin cambiar los datos). Reintentar programado. A2 luego intenta, tiene éxito. Cuando A1 intente nuevamente, abortará.

A1 tiene éxito pero informa de un error. Reintentar programado. La próxima vez que A1 o A2 intenten, abortarán.

Para que esto funcione, necesita realizar un seguimiento de si A1 y A2 se han completado, tal vez darles un UUID de tarea y almacenar una lista de tareas terminadas. O incluso solo usa la cola de tareas.

+0

Gracias, pero no estoy seguro de que eso funcione, ya que almacenar la identificación podría tener este problema ... pero creo que la respuesta de Nick anterior cubre mi caso. –

4

Editar: Esto es incorrecto - por favor vea los comentarios.

Tu código es correcto. La idempotencia a la que se refieren los documentos es con respecto a los efectos secundarios. Como explican los documentos, su función transaccional puede ejecutarse más de una vez; en tales situaciones si la función tiene algún efecto secundario, se aplicarán varias veces. Como su función de transacción no hace eso, estará bien.

Un ejemplo de una función problemática con respecto a idempotencia sería algo como esto:

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

En este caso, self.counter puede ser incrementado en 1, o potencialmente más de 1. Esto podría evitarse haciendo los efectos secundarios fuera de la transacción:

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

Gracias, Nick.¿Estás diciendo que cualquier operación de almacén de datos que haga en una transacción solo ocurrirá una vez, incluso si mi código obtiene una excepción y vuelve a intentarlo? Si mi transacción 'decrement' se llama dos veces debido a un nuevo intento, solo disminuirá una vez? En ndb-land, ¿eso se debe a que a mi transacción se le asigna una identificación que el almacén de datos sabe que ya ha cometido? –

+0

(y por "mi código ... reintentos" me refiero a "ndb retries para mí") –

+1

@ D.Hayes Ocurrirán varias veces, pero solo uno de ellos se comprometerá nuevamente con el almacén de datos. El almacén de datos usa simultaneidad optimista, por lo que cuando intenta comprometer una transacción, el almacén de datos comprueba si las entidades se han modificado desde que se obtuvieron y solo acepta la transacción si no lo han sido. –