Обработка состояния гонки в model.save ()

Как следует обрабатывать возможные условия гонки в методе модели 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

17
задан Shawn Chin 6 September 2011 в 15:49
поделиться

2 ответа

Вам это может казаться хаком, но для меня это выглядит как законная, разумная реализация подхода "оптимистического параллелизма" - попробуйте сделать что угодно, обнаружите конфликты, вызванные условиями гонки, если возникнет один, повторите попытку немного позже. Некоторые базы данных систематически используют это вместо блокировки, и это может привести к гораздо лучшей производительности, за исключением систем с большой нагрузкой на запись (которые довольно редки в реальной жизни).

Мне это очень нравится, потому что я вижу в этом общий случай принципа Хоппера: "легче попросить прощения, чем разрешения", который широко применяется в программировании (особенно, но не только в Python - язык, который обычно приписывают Хопперу, это, в конце концов, Cobol;-).

Одно из улучшений, которое я бы порекомендовал, это ждать случайное количество времени - чтобы избежать "состояния мета-гонки", когда два процесса пытаются одновременно, оба находят конфликты, и оба повторяют снова в одно и то же время, что приводит к "голоданию". time.sleep(random.uniform(0.1, 0.6)) или т.п. должно быть достаточно.

Более тонкое улучшение заключается в том, чтобы удлинить ожидание, если встречается больше конфликтов - это то, что известно как "экспоненциальный откат" в TCP/IP (конечно, вы не должны удлинять все экспоненциально, т.е. на постоянный множитель > 1 каждый раз, но этот подход имеет хорошие математические свойства). Это оправдано только для ограничения проблем для очень нагруженных записью систем (где множественные конфликты при попытке записи происходят довольно часто), и в вашем конкретном случае это, вероятно, не стоит делать.

15
ответ дан 30 November 2019 в 14:17
поделиться

Добавьте необязательное предложение FOR UPDATE в QuerySets http://code.djangoproject.com/ticket/2705

0
ответ дан 30 November 2019 в 14:17
поделиться
Другие вопросы по тегам:

Похожие вопросы: