2012-04-15 15 views
6

Per favore aiutami a trovare il mio fraintendimento.App Engine, transazioni e idempotency

Sto scrivendo un gioco di ruolo su App Engine. Alcune azioni che il giocatore prende consumano una certa statistica. Se la statistica raggiunge lo zero, il giocatore non può più eseguire azioni. Ho iniziato a preoccuparmi di imbrogliare i giocatori, però - cosa succede se un giocatore ha inviato due azioni molto velocemente, l'una accanto all'altra? Se il codice che decrementa la stat non è in una transazione, allora il giocatore ha la possibilità di eseguire l'azione due volte. Quindi, dovrei avvolgere il codice che decrementa le statistiche in una transazione, giusto? Fin qui tutto bene.

In GAE Python, però, abbiamo questo nel documentation:

Nota: Se la vostra applicazione riceve un'eccezione al momento della presentazione di una transazione, esso non significa sempre che l'operazione non è riuscita. È possibile ricevere le eccezioni Timeout, TransactionFailedError o InternalError nei casi in cui sono state eseguite transazioni e alla fine sarà applicato correttamente . Quando possibile, rendi le tue transazioni Datastore idempotenti così che se ripeti una transazione, il risultato finale sarà lo stesso.

Whoops. Ciò significa che la funzione Io correvo che assomiglia a questo:


def decrement(player_key, value=5): 
    player = Player.get(player_key) 
    player.stat -= value 
    player.put() 

Beh, questo non funzionera ', perché la cosa non è idempotente, giusto? Se inserisco un ciclo di tentativi intorno ad esso (ho bisogno di in Python? Ho letto che non ho bisogno di SO ... ma non riesco a trovarlo nei documenti) potrebbe incrementare il valore due volte, destra? Dato che il mio codice può catturare un'eccezione ma il datastore ha ancora eseguito il commit dei dati ... eh? Come posso risolvere questo? È questo un caso in cui ho bisogno di distributed transactions? Davvero?

+1

Beh, sì, ed è un buon punto ... ma prima di sparpagliare il mio codice con un gruppo di difficili da diagnosticare, difficili da riprodurre i bug mi piacerebbe sapere quale schema dovrei andare qui. –

+0

Il pattern è sulla strada giusta, ma GAE presenta alcune sfumature frustranti che rendono difficile un'implementazione chirurgicamente precisa come questa. Nella mia esperienza con GAE, a volte vale la pena, ea volte no. –

+1

@TravisWebb Non sono d'accordo. La sicurezza transazionale non è una "ottimizzazione prematura", né le collisioni tra le transazioni sono particolarmente improbabili. –

risposta

13

In primo luogo, la risposta di Nick non è corretta. La transazione di DHayes non è idempotente, quindi se viene eseguita più volte (ad esempio un tentativo quando si pensava che il primo tentativo fosse fallito, quando non lo era), il valore sarebbe stato decrementato più volte. Nick afferma che "il datastore controlla se le entità sono state modificate da quando sono state recuperate", ma ciò non impedisce il problema poiché le due transazioni hanno avuto recuperi separati e il secondo recupero è stato DOPO la prima transazione completata.

Per risolvere il problema, è possibile rendere la transazione idempotente creando una "chiave di transazione" e registrando quella chiave in una nuova entità come parte della transazione. La seconda transazione può controllare quella chiave di transazione e, se trovata, non farà nulla. La chiave della transazione può essere cancellata una volta che sei soddisfatto del completamento della transazione, oppure rinunci.

Mi piacerebbe sapere cosa significa "estremamente raro" per AppEngine (1 su un milione o 1 su un miliardo di dollari), ma il mio consiglio è che per le questioni finanziarie sono necessarie transazioni identi- tenziali. , ma non per i punteggi dei giochi, o anche "vite" ;-)

1

Non dovresti provare a memorizzare questo tipo di informazioni in Memcache, che è molto più veloce del Datastore (qualcosa che ti servirà se questa statistica è spesso usata nella tua applicazione). Memcache fornisce una bella funzione: decr che:

Decrementa atomicamente il valore di una chiave. Internamente, il valore è un numero intero a 64 bit senza segno. Memcache non controlla gli overflow a 64 bit. Il valore, se troppo grande, si avvolgerà.

Cerca decrhere. Dovresti quindi utilizzare un'attività per salvare il valore in questa chiave sul datastore ogni x secondi o quando viene soddisfatta una determinata condizione.

+0

Grazie per la risposta, ma non penso che funzionerà. Se fosse una specie di contatore globale che potrei permettermi valori sfocati per quello sarebbe perfetto, ma immagino che i giocatori sarebbero piuttosto arrabbiati se il valore fosse incasinato a causa di uno sfratto di memcache. –

+0

Si noti che la funzione decr() di Memcache anche "tappi decrescenti sotto zero a zero" [\ [1 \]] (https://cloud.google.com/appengine/docs/python/refdocs/google.appengine.api. memcache # google.appengine.api.memcache.Client.decr). – Lee

1

Se si pensa attentamente a ciò che si sta descrivendo, potrebbe non essere effettivamente un problema. Pensaci in questo modo:

Al giocatore è rimasto un punto stat. Quindi invia maliziosamente 2 azioni (A1 e A2) istantaneamente, ciascuna delle quali ha bisogno di consumare quel punto. Sia A1 che A2 sono transazionali.

Ecco cosa potrebbe accadere:

A1 riesce. A2 interromperà. Tutto bene.

A1 non riesce legittimamente (senza modificare i dati). Riprova in programma. A2 poi ci prova, ci riesce. Quando A1 prova di nuovo, abortirà.

A1 riesce ma segnala un errore. Riprova in programma. La prossima volta che A1 o A2 tenteranno, interromperanno.

Perché funzioni, è necessario tenere traccia di se A1 e A2 sono stati completati, è possibile assegnare loro un UUID di attività e memorizzare un elenco di attività completate? O anche solo usare la coda dei compiti.

+0

Grazie, ma non sono sicuro che funzioni, dal momento che la memorizzazione dell'ID stesso potrebbe incorrere in questo problema ... ma penso che la risposta di Nick sopra risponda al mio caso. –

4

Modifica: non è corretto, vedere i commenti.

Il tuo codice è a posto. L'idempotenza a cui si riferiscono i documenti riguarda gli effetti collaterali. Come spiegano i documenti, la tua funzione transazionale può essere eseguita più di una volta; in tali situazioni, se la funzione ha effetti collaterali, verranno applicate più volte. Dal momento che la tua funzione di transazione non lo fa, andrà bene.

Un esempio di una funzione problematico per quanto riguarda idempotence sarebbe qualcosa di simile:

def do_something(self): 
    def _tx(): 
    # Do something transactional 
    self.counter += 1 
    db.run_in_transaction(_tx) 

In questo caso, self.counter può essere incrementato di 1, o potenzialmente maggiore di 1. Questo potrebbe essere evitata facendo gli effetti collaterali al di fuori della transazione:

def do_something(self): 
    def _tx(): 
    # Do something transactional 
    return 1 
    self.counter += db.run_in_transaction(_tx) 
+0

Grazie, Nick.Stai dicendo che qualsiasi operazione di datastore che faccio in una transazione avverrà solo una volta, anche se il mio codice ottiene un'eccezione e lo ritenta? Se la mia transazione 'decrement' viene chiamata due volte a causa di un nuovo tentativo, verrà decrementata solo una volta? In ndb-land, è perché la mia transazione viene assegnata all'ID che il datastore sa di aver già commesso? –

+0

(e per "il mio codice ... riprova" intendo "ndb tentativi per me") –

+1

@ D.Hayes Accadranno più volte, ma solo uno di essi verrà reimpostato sul datastore. Il datastore utilizza la concorrenza ottimistica, quindi quando tenta di eseguire il commit di una transazione, il datastore controlla se le entità sono state modificate da quando sono state recuperate e accetta solo la transazione se non lo sono state. –