Обработка состояния гонки в model.save () - PullRequest
16 голосов
/ 19 августа 2010

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

Я встречал @ транзакций .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)

Есть предложения?

Обновление:

Другая проблема с вышеприведенным решением состоит в том, что цикл while никогда не завершится, если IntegrityError вызван nameконфликт (или любое другое уникальное поле в этом отношении).

Для справки, вот что у меня есть, что, кажется, делает то, что мне нужно:

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 ]

15 голосов
/ 19 августа 2010

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

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

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

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

0 голосов
/ 21 февраля 2012

Я использую решение Шона Чина, и оно оказывается очень полезным.Единственное изменение, которое я сделал, это заменил

self.position = self.parent.item_count

на

self.position = self.parent.latest('position').position

, просто чтобы убедиться, что я имею дело с последним номером позиции (который в моем случае может не быть item_count, потомунекоторых зарезервированных неиспользованных позиций)

0 голосов
/ 19 августа 2010

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

...