Domanda

Come si deve gestire una possibile condizione di gara in modo save() di un modello?

Ad esempio, l'esempio seguente implementa un modello con un elenco di elementi correlati ordinato. Quando si crea una nuova voce la dimensione lista corrente viene utilizzata come la sua posizione.

Da quello che posso dire, questo può andare male se gli oggetti multipli sono creati simultaneamente.

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)

ho incontrato @transactions .commit_on_success() ma che sembra applicarsi solo alla vista. Anche se si applica ai metodi del modello, ancora non saprei come gestire correttamente una transazione non riuscita.

sto currenly maneggiarlo in questo modo, ma ci si sente più simile a un hack che una soluzione

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)

Qualche suggerimento?

Aggiornamento:

Un altro problema con la soluzione di cui sopra è che il ciclo while finirà mai se IntegrityError è causato da un conflitto name (o qualsiasi altro campo univoco per questo).

Per la cronaca, ecco cosa ho finora che sembra fare quello che mi serve:

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
È stato utile?

Soluzione

Può sensazione come un hack a voi, ma a me sembra una legittima, ragionevole attuazione dell'approccio "ottimista concorrenza" - provare a fare qualunque cosa, rilevare i conflitti causati da condizioni di gara, se si verifica, riprovare un po 'più tardi. Alcuni database sistematicamente usi che invece di bloccaggio, e può portare a prestazioni molto meglio se non sotto sistemi sotto un molto di write-carico (sono abbastanza rari nella vita reale).

Mi piace molto perché lo vedo come un caso generale del Principio Hopper: "E 'facile chiedere perdono che il permesso", che si applica ampiamente in programmazione (in particolare, ma non esclusivamente in Python - la lingua Hopper è di solito accreditato per è, dopo tutto, Cobol, -).

Un miglioramento vi consiglio è quello di attendere un casuale quantità di tempo - evitare una "condizione di meta-gara", dove due processi cercano allo stesso tempo, entrambi i conflitti trovare, ed entrambi retry nuovo , allo stesso tempo, che porta a "morire di fame". time.sleep(random.uniform(0.1, 0.6)) o simili dovrebbero essere sufficienti.

Un miglioramento più raffinato è quello di allungare l'attesa prevista la conformità ai più conflitti - questo è ciò che è noto come "backoff esponenziale" in TCP / IP (che non avrebbe dovuto alle cose allunga in modo esponenziale, vale a dire da un moltiplicatore costante > 1 ogni volta, naturalmente, ma questo approccio ha delle belle proprietà matematiche). E 'garantito solo a problemi limite per molto sistemi di scrittura-caricato (in cui più conflitti durante tentato scrive capita molto spesso) e non può probabilmente essere valsa la pena nel vostro caso specifico.

Altri suggerimenti

Aggiungi clausola FOR UPDATE opzionale per querysets http://code.djangoproject.com/ticket/2705

Io uso la soluzione di Shawn Chin e si rivela molto utile. L'unico cambiamento che ho fatto è stato quello di sostituire il

self.position = self.parent.item_count

con

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

solo per assicurarsi che ho a che fare con l'ultimo numero di posizione (che nel mio caso potrebbe non essere ITEM_COUNT a causa di alcune posizioni non utilizzate riservate)

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top