Обработка состояния гонки в 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)

Я столкнулся с @transaction .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

Ответы

Ответ 1

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

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

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

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

Ответ 3

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

self.position = self.parent.item_count

с

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

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