24

How do I handle concurrency in a Django model? I don't want the changes to the record being overwritten by another user who reads the same record.

1

4 Answers 4

30

The short answer, this really isn't a Django question as presented.

Concurrency control is often presented as a technical question, but is in many ways a question of functional requirements. How do you want/need your application to work? Until we know that, it will be difficult to give any Django-specific advice.

But, I feel like rambling, so here goes...

There are two questions that I tend to ask myself when confronted with the need for concurrency control:

  • How likely is it that two users will need to concurrently modify the same record?
  • What is the impact to the user if his/her modifications to a record are lost?

If the likelihood of collisions is relatively high, or the impact of losing a modification is severe, then you may be looking at some form of pessimistic locking. In a pessimistic scheme, each user must acquire a logical lock prior to opening the record for modification.

Pessimistic locking comes with much complexity. You must synchronize access to the locks, consider fault tolerance, lock expiration, can locks be overridden by super users, can users see who has the lock, so on and so on.

In Django, this could be implemented with a separate Lock model or some kind of 'lock user' foreign key on the locked record. Using a lock table gives you a bit more flexibility in terms of storing when the lock was acquired, user, notes, etc. If you need a generic lock table that can be used to lock any kind of record, then take a look at the django.contrib.contenttypes framework, but quickly this can devolve into abstraction astronaut syndrome.

If collisions are unlikely or lost modifications are trivially recreated, then you can functionally get away with optimistic concurrency techniques. This technique is simple and easier to implement. Essentially, you just keep track of a version number or modification time stamp and reject any modifications that you detect as out of whack.

From a functional design standpoint, you only have to consider how these concurrent modification errors are presented to your users.

In terms of Django, optimistic concurrency control can be implemented by overriding the save method on your model class...

def save(self, *args, **kwargs):
    if self.version != self.read_current_version():
        raise ConcurrentModificationError('Ooops!!!!')
    super(MyModel, self).save(*args, **kwargs)

And, of course, for either of these concurrency mechanisms to be robust, you have to consider transactional control. Neither of these models are fully workable if you can't guarantee ACID properties of your transactions.

Sign up to request clarification or add additional context in comments.

6 Comments

Ok, that last example you wrote was what I wanted to know, so I have to write my own save method for the model. I come from another framework when it's a matter of setting a property to "compareallvalues" to a control, so I had no idea how to implement it in Django, and I didnt find any example in the starting tutorial or I missed it. I thought that since Django automates several tasks that in other frameworks are done by the programmer, this task could be automatized like in the framework I refered first
Yeah, my personal opinion is that frameworks should avoid generalizing these design patterns because of all the functional/technical implications. YMMV...
As mentioned below the code in the last snippet is broken. There still might appear a modification between version check and save method.
@julkiewicz As I noted in the final paragraph of my answer, it only works if it's executing in the context of an atomic DB transaction. This isn't demonstrated in my code snippet because it would presumably be handled in a higher layer of code.
Ok, however one thing worth noting is that it's very common for the databases not to restart their transactions automatically once a deadlock emerges. And the above code can lead to a deadlock.
|
14

I don't think that 'keeping a version number or timestamp' works.

When self.version == self.read_current_version() is True, there is still a chance that the version number got modified by other sessions just before you call super().save().

1 Comment

Without locking the table in question, this is correct. However, there are decorators for simplifying table locking with Django models, which should avoid the race condition you're referring to.
5

I agree with the introductory explanation from Joe Holloway.

I want to contribute with a working snippet relative to the very last part of his answer ("In terms of Django, optimistic concurrency control can be implemented by overriding the save method on your model class...")

You can use the following class as an ancestor for your own model.

If you are inside a database transaction (for example, by using transaction.atomic in an outer scope), the following Python statements are safe and consistent

In practice via one single shot, the statements filter + update provide a sort of test_and_set on the record: they verify the version and acquire an implicitly database-level lock on the row.

So the following "save" is able to update fields of the record sure it is the only session which operates on that model instance.

The final commit (for example executed automatically by _exit_ in transaction.atomic) releases the implicit database-level lock on the row:

class ConcurrentModel(models.Model):
    _change = models.IntegerField(default=0)

    class Meta:
        abstract = True

    def save(self, *args, **kwargs):
        cls = self.__class__
        if self.pk:
            rows = cls.objects.filter(
                pk=self.pk, _change=self._change).update(
                _change=self._change + 1)
            if not rows:
                raise ConcurrentModificationError(cls.__name__, self.pk)
            self._change += 1
        super(ConcurrentModel, self).save(*args, **kwargs)

It is taken from https://bitbucket.org/depaolim/optlock/src/ced097dc35d3b190eb2ae19853c2348740bc7632/optimistic_lock/models.py?at=default

Comments

0

For Concurrency control, I use and recommend pessimistic lock with select_for_update() more than optimistic lock which are Joe Holloway's answer and Marco De Paoli's answer.

You can also see my posts below about SELECT FOR UPDATE in Django Admin:

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.