How should one handle a potential race symptom in a model's save() method?

For instance, the next example implements one by having an purchased listing of related products. When designing a brand new Item the present list dimensions are used since it's position.

From what I will tell, this could fail if multiple Products are produced at the same time.

class OrderedList(models.Model):
    # ....
    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
            # use item count as next position number
            self.position = parent.item_count
        super(Item, self).save(*args, **kwargs)

I have run into @transactions.commit_on_success() but that appears to use simply to sights. Even when it did affect model techniques, I still wouldn't understand how to correctly handle a unsuccessful transaction.

I'm currenly handling it like so, however it feels a lot more like a hack than the usual solution

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

Any suggestions?


One other issue using the above solution would be that the while loop won't ever finish if IntegrityError is triggered with a name conflict (or other unique area for your matter).

For that record, some tips about what I've to date which appears to complete things i need:

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

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

It might feel just like a hack for you, but in my experience it appears just like a legitimate, reasonable implementation from the "positive concurrency" approach -- try doing whatever, identify conflicts triggered by race conditions, if a person happens, retry a little later. Some databases methodically uses that rather than securing, and it can result in far better performance except under systems within lot of write-load (that are quite rare in tangible existence).

I love it a great deal because I view it like a general situation from the Hopper Principle: "it's not hard to request forgiveness than permission", which is applicable broadly in programming (especially although not solely in Python -- the word what Hopper is generally credited for is, in the end, Cobol-).

One improvement I'd recommend would be to wait a random period of time -- avoid a "meta-race condition" where two processes try simultaneously, both find conflicts, and both retry again simultaneously, resulting in "starvation". time.sleep(random.uniform(0.1, 0.6)) or even the like should suffice.

A far more refined improvement would be to lengthen the expected wait if more conflicts are met -- this really is what is known "exponential backoff" in TCP/IP (you would not need to lengthen things tremendously, i.e. with a constant multiplier > 1 every time, obviously, but that approach has nice mathematical qualities). It is just warranted to limit trouble for very write-loaded systems (where multiple conflicts throughout attempted creates happen quite frequently) also it may very well 't be worthwhile inside your specific situation.

Add optional FOR UPDATE clause to QuerySets