How should one handle a potential race symptom in a model's
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): # .... @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 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.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)
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 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
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 http://code.djangoproject.com/ticket/2705