Avoiding DB Race Conditions in Django

2014-03-20 | #django, #python, #solution, #webdev

This is evil. If you have a view that is called in fast succession or running longer than your average request you might walk into problems correctly managing database flags for mail sending e.g.

You can avoid this by using Django's select_for_update-queryset-method, which locks a row until a transaction is finished, letting all subsequent requests wait for the first view. So parallel requests don't trigger critical stuff twice or more.

Normally Django autocommits every request, using transaction.autocommit. To get rid of that, you'll need to use a different transaction decorator, to exclude your view from this behaviour. The most convenient in this case should be commit_on_success, which auto-commits on return from the view and auto-rollbacks on an exception. In both cases releasing the lock.

from django.db import transaction

@transaction.atomic

def get_stuff(request, id):

try:

# use select_for_update() to lock the rows and nowwait=False to keep other requests waiting until the lock is released

stuff = Stuff.objects.select_for_update(nowait=False).get(id=id)

except Stuff.DoesNotExist:

return HttpResponseNotFound('stuff does not exist')

if not stuff.has_flag:

stuff.has_flag = True

stuff.save()

critial_method()

For older Django versions you may need to use

@transaction.commit_on_success