Search code examples
djangotransactionsrace-condition

Django: Maintaining a counter in a field (race condition)


I have a model similar to this example

class Foo(models.Model):
    a = models.ForeignKey(...)
    number = models.IntegerField()

    @transaction.atomic
    def save(self, commit=True):
        if self.pk is None:            
            current_max = (
                Foo.objects
                .filter(a=self.a)
                .order_by('-number')
                .first()
            )
            current_max = 0 if current_max is None else current_max.number
            self.number = current_max + 1
        return super().save(commit)

The idea is that for each a, there will be a series of Foos numbered from 1 and then onwards.

The issue is that, even though we have @transaction.atomic, there is a race condition, because the transaction isolation level that Django expects will allow the transactions to run simultaneously, i.e.

A -> Get max -> 42
B -> Get max -> 42
A -> Set max + 1
A -> save
B -> Set max + 1
B -> save
Both will be 43

So how would I go about solving that? Is there a way to atomically setting the counter so I don't get the race condition between retrieving the current max and inserting the new value?

This question is similar to this one, but different enough that that question did not provide an answer to my specific example


Solution

  • Check out select_for_update. As described in the docs:

    Returns a queryset that will lock rows until the end of the transaction, generating a SELECT ... FOR UPDATE SQL statement on supported databases.

    Alternatively, there's two approaches defined by Haki Benata at https://web.archive.org/web/20170707121253/https://medium.com/@hakibenita/how-to-manage-concurrency-in-django-models-b240fed4ee2 which may be of interesting reading

    Finally, if you need to lock the entire table, there's a method described that would allow you to do that. Effectively, you create a locking context manager that acquires a full table lock and releases it on exit