Handling race condition in model.save()

Viewed 5048

How should one handle a possible race condition in a model's save() method?

For example, the following example implements a model with an ordered list of related items. When creating a new Item the current list size is used as its position.

From what I can tell, this can go wrong if multiple Items are created concurrently.

class OrderedList(models.Model):
    # ....
    @property
    def item_count(self):
        return self.item_set.count()

class Item(models.Model):
    # ...
    name   = models.CharField(max_length=100)
    parent = models.ForeignKey(OrderedList)
    position = models.IntegerField()
    class Meta:
        unique_together = (('parent','position'), ('parent', 'name'))

    def save(self, *args, **kwargs):
        if not self.id:
            # use item count as next position number
            self.position = parent.item_count
        super(Item, self).save(*args, **kwargs)

I've come across @transactions.commit_on_success() but that seems to apply only to views. Even if it did apply to model methods, I still wouldn't know how to properly handle a failed transaction.

I am currenly handling it like so, but it feels more like a hack than a solution

def save(self, *args, **kwargs):
    while not self.id:
        try:
            self.position = self.parent.item_count
            super(Item, self).save(*args, **kwargs)
        except IntegrityError:
            # chill out, then try again
            time.sleep(0.5)

Any suggestions?

Update:

Another problem with the above solution is that the while loop will never end if IntegrityError is caused by a name conflict (or any other unique field for that matter).

For the record, here's what I have so far which seems to do what I need:

def save(self, *args, **kwargs):   
    # for object update, do the usual save     
    if self.id: 
        super(Step, self).save(*args, **kwargs)
        return

    # for object creation, assign a unique position
    while not self.id:
        try:
            self.position = self.parent.item_count
            super(Step, self).save(*args, **kwargs)
        except IntegrityError:
            try:
                rival = self.parent.item_set.get(position=self.position)
            except ObjectDoesNotExist: # not a conflict on "position"
                raise IntegrityError
            else:
                sleep(random.uniform(0.5, 1)) # chill out, then try again
3 Answers
Related