App Engine, transacciones e idempotency

Por favor ayúdame a encontrar mi malentendido.

Estoy escribiendo un juego de rol en App Engine. Ciertas acciones que el jugador toma consumen cierta estadística. Si la estadística llega a cero, el jugador no puede realizar más acciones. Sin embargo, empecé a preocuparme por engañar a los jugadores. ¿Qué sucede si un jugador envía dos acciones muy rápidamente, una al lado de la otra? 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, debería envolver el código que disminuye la estadística en una transacción, ¿verdad? Hasta ahora tan bueno.

En GAE Python, sin embargo, tenemos esto en la documentación :

Nota : si su aplicación recibe una excepción al enviar una transacción, no siempre significa que la transacción haya fallado. Puede recibir excepciones de Timeout, TransactionFailedError o InternalError en los casos en que las transacciones se hayan confirmado y finalmente se apliquen correctamente. Siempre que sea posible, haga que sus transacciones del almacén de datos sean idempotentes para que, si repite una transacción, el resultado final sea el mismo.

Whoops. Eso significa que la función que estaba ejecutando se parece a esto:

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 bucle de rebash a su alrededor (¿necesito hacerlo en Python? He leído que no necesito hacerlo en SO … pero no puedo encontrarlo en la documentación) podría boost el valor dos veces, ¿Correcto? Dado que mi código puede detectar una excepción, el almacén de datos aún confirmó los datos … ¿eh? ¿Cómo puedo solucionar esto? ¿Es este un caso donde necesito transacciones distribuidas ? ¿De verdad?

Primero, la respuesta de Nick no es correcta. La transacción de DHayes no es idempotente, por lo que si se ejecuta varias veces (es decir, un rebash cuando se pensó que el primer bash falló, cuando no lo hizo), entonces el valor se habrá reducido varias veces. Nick dice que “el almacén de datos verifica si las entidades han sido modificadas desde que fueron recuperadas”, pero eso no evita el problema ya que las dos transacciones tenían búsquedas separadas, y la segunda búsqueda fue DESPUÉS de la primera transacción completada.

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 se puede eliminar una vez que esté satisfecho de que la transacción se haya completado, o deje de reintentar.

Me gustaría saber qué significa “extremadamente raro” para AppEngine (¿1 en un millón o en un billón?), Pero mi consejo es que se requieren transacciones idempotentes para asuntos financieros, pero no para puntajes de juego, o incluso “vidas” 😉

Edición: Esto es incorrecto – por favor vea los comentarios.

Tu código está bien. La identidad que mencionan los documentos se refiere 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 efectos secundarios, se aplicarán varias veces. Ya que la función de transacción no hace eso, estará bien.

Un ejemplo de una función problemática con respecto a la 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 incrementarse 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) 

¿No debería intentar almacenar este tipo de información en Memcache, que es mucho más rápido que el almacén de datos (algo que necesitará si esta estadística se usa con frecuencia en su aplicación)? Memcache te proporciona una buena función: decr que:

Atómicamente disminuye el valor de una clave. Internamente, el valor es un entero de 64 bits sin signo. Memcache no comprueba desbordamientos de 64 bits. El valor, si es demasiado grande, se envolverá alrededor.

Busca decr aquí . Luego debe usar una tarea para guardar el valor de esta clave en el almacén de datos cada x segundos o cuando se cumpla una determinada condición.

Si piensa cuidadosamente acerca de lo que está describiendo, puede que en realidad no sea un problema. Piensa en ello de esta manera:

Tu jugador tiene un punto de estadísticas a la izquierda. A continuación, envía maliciosamente 2 acciones (A1 y A2) de forma instantánea, las cuales necesitan consumir ese punto. Tanto A1 como A2 son transaccionales.

Esto es lo que podría pasar:

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

A1 falla legítimamente (sin cambiar los datos). Reintentar progtwigdo. A2 entonces intenta, tiene éxito. Cuando A1 vuelva a intentarlo, abortará.

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

Para que esto funcione, debe estar al tanto de si A1 y A2 se han completado. ¿Puede darles un UUID de tareas y almacenar una lista de tareas terminadas? O incluso simplemente usar la cola de tareas.