I'm trying to use Django 1.6 transactions to avoid race conditions on a game I'm developing. The game server has one simple goal: to pair two players.
My current approach is:
This is the code:
# data['nickname'] = user's choice
games = GameConnection.objects.all()
if not games:
game = GameConnection.objects.create(connection=unicode(uuid.uuid4()))
game.nick1 = data["nickname"]
game.save()
response = HttpResponse(json.dumps({'connectionId': game.connection, 'whoAmI': 1, 'nick1': game.nick1, 'nick2': ""}))
else:
game = games[0]
conn = game.connection
nick1 = game.nick1
nick2 = data["nickname"]
game.delete()
response = HttpResponse(json.dumps({'connectionId': conn, 'whoAmI': 2, 'nick1': nick1, 'nick2': nick2}))
return response
Obviously there is a race condition on the code above. As this code is not atomic, it can happen that:
I tried do but this whole block under with transaction.atomic():
, or to use the @transaction.atomic
decorator. But still, I am able to reproduce the race condition.
I am sure there is something about the transaction dynamics I am missing here. Can anyone shed a light?
See below for details. Django uses transactions or savepoints automatically to guarantee the integrity of ORM operations that require multiple queries, especially delete () and update () queries. Django’s TestCase class also wraps each test in a transaction for performance reasons.
If your MySQL setup does not support transactions, then Django will always function in autocommit mode: statements will be executed and committed as soon as they’re called. If your MySQL setup does support transactions, Django will handle transactions as explained in this document.
This problem cannot occur in Django’s default mode and atomic () handles it automatically. Inside a transaction, when a call to a PostgreSQL cursor raises an exception (typically IntegrityError ), all subsequent SQL in the same transaction will fail with the error “current transaction is aborted, queries ignored until end of transaction block”.
When exiting an atomic block, Django looks at whether it’s exited normally or with an exception to determine whether to commit or roll back. If you catch and handle exceptions inside an atomic block, you may hide from Django the fact that a problem has happened. This can result in unexpected behavior.
@Sai is on track... the key is that the lock/mutex won't occur until a write (or delete). As coded, there will always be a time between "discovery" (read) of the pending connection and "claim" (write/lock) of the pending connection, with no way to know that a connection is in the process of being claimed.
If you are using PostgreSQL (pretty sure MySQL supports it, too), you can force the lock with "select for update", which will prevent another request from getting the same row until the transaction completes:
game = GameConnection.objects.all()[:1].select_for_update()
if game:
#do something, update, delete, etc.
else:
#create
Final note - consider something other than all()
to be explicit about which game might be picked up (e.g., order by a "created" timestamp or something). Hope that helps.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With