Frage

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:

  1. User wants to play
  2. The server checks if there is anyone else waiting to play.
    1. If there is not, it creates a GameConnection object (that has a unique identifier - uuid4).
    2. If there is, it gets the GameConnection identifier and deletes the GameConnection.

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:

  • A checks for game connections. Finds none.
  • A creates a game connection.
  • B checks for game connections. Finds one (A).
  • C checks for game connections. Finds one (A).
  • B gets A's connection identifier and starts a game.
  • C gets A's connection identifier and starts a game.

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?

War es hilfreich?

Lösung

@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.

Lizenziert unter: CC-BY-SA mit Zuschreibung
Nicht verbunden mit StackOverflow
scroll top