Как следует обрабатывать возможные условия гонки в методе модели save ()
?
Например, в следующем примере реализована модель с упорядоченным списком. связанных предметов. При создании нового элемента в качестве его позиции используется текущий размер списка.
Из того, что я могу сказать, это может пойти не так, если несколько элементов создаются одновременно.
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)
Я встречал @transactions .commit_on_success ()
, но, похоже, это относится только к взгляды. Даже если бы это действительно применимо к модельным методам, я все равно не знал бы, как правильно обрабатывать неудачные транзакции.
Я сейчас так обрабатываю это, но это больше похоже на взлом, чем на решение
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)
Есть предложения?
Другая проблема с вышеприведенным решением состоит в том, что цикл , в то время как
никогда не завершится, если IntegrityError
вызван конфликтом имени
(или любое другое уникальное поле в этом отношении).
Для справки, вот что у меня есть, которое, кажется, делает то, что мне нужно:
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
Вам это может казаться хаком, но для меня это выглядит как законная, разумная реализация подхода "оптимистического параллелизма" - попробуйте сделать что угодно, обнаружите конфликты, вызванные условиями гонки, если возникнет один, повторите попытку немного позже. Некоторые базы данных систематически используют это вместо блокировки, и это может привести к гораздо лучшей производительности, за исключением систем с большой нагрузкой на запись (которые довольно редки в реальной жизни).
Мне это очень нравится, потому что я вижу в этом общий случай принципа Хоппера: "легче попросить прощения, чем разрешения", который широко применяется в программировании (особенно, но не только в Python - язык, который обычно приписывают Хопперу, это, в конце концов, Cobol;-).
Одно из улучшений, которое я бы порекомендовал, это ждать случайное количество времени - чтобы избежать "состояния мета-гонки", когда два процесса пытаются одновременно, оба находят конфликты, и оба повторяют снова в одно и то же время, что приводит к "голоданию". time.sleep(random.uniform(0.1, 0.6))
или т.п. должно быть достаточно.
Более тонкое улучшение заключается в том, чтобы удлинить ожидание, если встречается больше конфликтов - это то, что известно как "экспоненциальный откат" в TCP/IP (конечно, вы не должны удлинять все экспоненциально, т.е. на постоянный множитель > 1 каждый раз, но этот подход имеет хорошие математические свойства). Это оправдано только для ограничения проблем для очень нагруженных записью систем (где множественные конфликты при попытке записи происходят довольно часто), и в вашем конкретном случае это, вероятно, не стоит делать.
Добавьте необязательное предложение FOR UPDATE в QuerySets http://code.djangoproject.com/ticket/2705