tutorial - heroku web python
Evitando las condiciones de carrera, Django+Heroku+PostgreSQL (1)
Dirijo un sitio de concurso en el que intentas hacer clic en el número X para ganar un premio. Está escrito en Django y se ejecuta en Heroku con PostgreSQL. Cada clic se guarda como una instancia de un modelo Play, que calcula su número viendo cuántas reproducciones hay en el DB anterior y agrega 1. Este número se guarda en el modelo Play. Esto es fundamental para todo el sitio, ya que el juego de números que realizas determina si obtienes o no un premio.
Recientemente, tuvimos un caso en el que 2 personas obtuvieron el número ganador al mismo tiempo. Al revisar la base de datos, veo que en realidad hay un 3% de reproducciones que comparten sus números. Oops. He agregado ''unique_together'' al campo '' número '' y '' juego '' del modelo Play, por lo que el DB me ayudará a evitar números repetidos en el futuro, pero me preocupa que las condiciones futuras de carrera puedan hacer que el sistema salte algunos números, lo que haría Sería malo si los números en cuestión fueran números ganadores.
He investigado el bloqueo de la tabla, pero me preocupa que esto pueda matar la concurrencia de sitios (actualmente tenemos hasta 500 jugadores simultáneos y esperamos mucho más en el futuro).
¿Qué estrategia debo implementar también, estar 100% seguro de que nunca tengo números repetidos u omitidos?
Mi clase de juego:
class Play(models.Model):
token = models.CharField(unique=True, max_length=200)
user = models.ForeignKey(User)
game = models.ForeignKey(Game)
datetime = models.DateTimeField(auto_now_add=True)
winner = models.BooleanField(default=False)
flagged = models.BooleanField(default=False)
number = models.IntegerField(blank=True, null=True)
ip = models.CharField(max_length=200, blank=True, null=True)
def assign_number(self, x=1000):
#try to assign number up to 1000 times, in case of race condition
while x > 0:
before = Play.objects.filter(id__lt=self.id, game=self.game)
try:
self.number = before.count()+1
self.save()
x=0
except:
x-=1
class Meta:
unique_together = ((''user'', ''game'', ''datetime''), (''game'',''number''))
Una solución simple sería poner el contador y el usuario ganador en el modelo del juego. A continuación, puede usar select_for_update
para bloquear el registro:
game = Game.objects.select_for_update().get(pk=gamepk)
if game.number + 1 == X
# he is a winner
game.winner = request.user
game.number = game.number + 1
game.save()
else:
# u might need to stop the game if a winner already decided
Como parte de la misma transacción, también puedes grabar los objetos del Player
para que también sepas quién hizo clic y rastrear otra información, pero no coloques el número y el ganador allí. Para usar select_for_update
necesitas usar postgresql_psycopg2
backend.
Actualización: dado que django establece el autocommit por defecto, debes ajustar el código anterior en la transacción atómica. De django docs
Seleccione para la actualización Si confiaba en "transacciones automáticas" para proporcionar el bloqueo entre select_for_update () y una subsiguiente> operación de escritura, un diseño extremadamente frágil, pero no obstante posible, debe envolver el código correspondiente en atomic ().
Puede decorar su vista con @transaction.atomic
:
from django.db import transaction
@transaction.atomic
def viewfunc(request):
# This code executes inside a transaction.
do_stuff()