Qualche tempo addietro usando l'SDK Microsoft.Azure.Mobile.Client in una app Xamarin.Forms che lavora eseguendo sync dei dati con modalità off-line, mi sono accorto della presenza di alcuni errori HTTP Error 412.0 - Precondition Failed.
Il codice usa un backend scritto in C# e Azure Sql Server: nulla di esotico o particolare: classica logica di funzionamento esposto da tutte le best practice e codice di esempio Microsoft.
Comunque investigando ho potuto verificare che in realtà anche in presenza di questa segnalazione tutto funzionava in modo corretto: il sync veniva eseguito compiutamente e I dati erano correttamente presenti in Sql Azure. Inoltre, come recita la dicitura, si tratta di pre-errori........
Comunque la cosa mi ha incuriosito e investigando sulle origini della segnalazione sopra ho compreso cosa succede “dietro le quinte” dell'optimistic concurrency features in Azure Mobile Services client SDK.
In questo post Vi presento i miei appunti di studio, preceduti da un mimimo di teoria.Ovviamente per saperne di più potete vedere i link più interessanti che ho trovato e che ho inserito diligentemente in Linkografia.
Le moderne applicazioni sono in grado di esporre lo stesso set di dati a più utilizzatori, che possono eventualmente fare delle modifiche su di esso: questo possibilità introduce dei problemi relativi alla gestione della concorrenza di dette modifiche, e che possono avvenire sulle stesse tuple.
Lavorare in modalità sync offline, cioè in contesti in cui l'applicazione è disconnessa e solo periodicamente viene eseguta una sincronizzazione che riporta le eventuali modifiche su Azure e riaggiorna l'intero set di dati, non fa che acuire la situazione.
Esistono tre strategie per gestire la concorrenza: Optimistic concurrency, Pessimistic concurrency, Last writer wins. In questo caso esporremo solo la prima, essendo quella più utilizzata nel caso di sync off line.
Il caso della Optimistic concurrency si basa sull'idea che una applicazione che ha eseguito una modifica su una riga, in fase di sync l'sdk si troverà nel caso di inoltratre la richiesta di update di detta riga alla parte server: quindi in questa fase il sistema verificherà, prima di apportare alcun aggiornamento, che la tupla in oggetto non sia stata cambiata precedentemente da altre applicazioni.
App utente A preleva dati da Sql Azure. App utente B preleva stesso set di dati da Sql Azure. App utente A modifica una riga. App utente B modifica la stessa riga. App utente A sicronizza con Azure Sql Server e riporta le modifche sul database. App utente B sicronizza con Azure Sql Server e riporta le modifche sul database ??
Il problema qui è che B ha modificato una “vecchia versione” della tupla: inoltre in fase di sincronizzazione quale delle due modifiche deve vincere ? Quella già presente su database, quindi quella inoltrata dalla app dell'utente A (perdendo di conseguenze le modifiche di B), oppure quella dell'utente B (perdendo, così, le modiifche dell'utente A) ?
Per gestire la versione della tupla viene usato il campo version, che deve essere presente sul database e il relativo valore viene interamente gestito dall'sdk. Questo campo rappresenta un valore univoco che è aggiornato ogni volta che viene eseguita una modifica sulla riga stessa.
Occorre osservare che questo campo viene utilizzato non solo gestire la concorrenza optimistic concurrency delle modifiche, ma anche per introdurre una più efficare gestione del recupero delle righe da parte dell'applicazione (conditional retrieval).
Conditional retrieval
Il concetto di conditional retrieval è il seguente: in fase di sincronizzazione dei dati se si ha sull'applicazione la stessa versione della tupla memorizzata sul lato server è possibile risparmiare un pò di traffico internet, e cpu dei dispositivi coinvolti, ottenendo dal server stesso una risposta che conferma questo fatto senza rispedire inutilmente l'intera riga all'applicazione. In caso contrario allora il server rispedirà la tupla.
Conditional Updates
Per gestire la concorrenza optimistic nell'sdk Azure vengono impiegate le conditional updates. E' questa la parte che genera la segnalazione oggetto del post.
In fase di sincronizzazione dell'applicazione, in cui si vogliono inoltrare al server le modifiche apportate alle varie righe (fase di Push), per ogni riga l'sdk inoltrerà al server una richiesta di modifica della tupla (conditional update) tramite PATCH, proponendo nella trama della richiesta stessa la versione che è stata oggetto della modifica.
Oss.: Ovviamente tutto questo avverrà senza esplicitamente scrivere alcuna riga di codice: semplicemente invocando il metodo push dell'sdk.
Se questa è la stessa presente sul database allora vuol dire che non Vi sono state modifiche precedenti da parte di altri, e quindi la modifica viene accettata, altrimenti il server risponde con un Precondition Failed.
Ecco nel dettaglio il messaggio invianto dal client per modificare una riga: molti campi sono stati eliminati per chiarezza
PATCH /tables/attivita/2A6025E7-0538-47B2-BD9F-186923F96E0F? __systemProperties=version HTTP/1.1 Content-Type: application/json Host: infpress.azure-mobile.net If-Match: "AAAAAAAACBM=" {"id":"2A6025E7-0538-47B2-BD9F-186923F96E0F","text":"leggere blog informatica pressapochista"}
Come si può vedere viene inviata la modifica, ma usando il campo del protocollo HTTP If-Match si inoltra al server anche la versione che è stata modificata dal client.
Se tale versione è la stessa presente sul database allora il server risponderà con il messaggio seguente, che conterrà nel campo ETAG il nuovo valore del campo version (che sarà applicato alla medesima riga sul client).
HTTP/1.1 200 OK Cache-Control: no-cache Content-Length: 98 Content-Type: application/json ETag: "AAAAAAAACBU=" Server: Microsoft-IIS/8.0 Date: Fri, 22 Nov 2013 23:57:47 GMT {"id":"2A6025E7-0538-47B2-BD9F-186923F96E0F","text":"leggere blog informatica pressapochista"}
Nello sfortunato caso in cui qualche altra app abbia modifcato la medesima riga allora il server risponderà con un messaggio come questo nel seguito.
HTTP/1.1 412 Precondition Failed Cache-Control: no-cache Content-Length: 114 Content-Type: application/json ETag: "AAAAAAAACBU=" {"id":"2A6025E7-0538-47B2-BD9F-186923F96E0F","__version":"AAAAAAAACBU=","text":"NON leggere mai più informatica pressapochista "}
E' possibile notare che verrà proposto non solo l'avviso che la riga è stata precedentemente modificata, ma viene anche inoltrata la versione corrente presente suk server che contiene dette modifiche.Questo con l'evidente scopo di ottimizzare la banda nelle comunicazioni tra client e server.
Questa situazione è quella che provoca il messaggio di errore oggetto di questo post. In un caso come questo Azure SDK non può fare nulla: si entra nella semantica dei dati dell'applicazione, e quindi è lo sviluppatore dell'applicazione che deve decidere “da codice” cosa fare.
Per gestire una situzione come questa occorre eseguire un catch di MobileServicePreconditionFailedException o MobileServicePushFailedException.
try { await MobileService.SyncContext.PushAsync(); } catch (MobileServicePushFailedException ex) { ....... }
L'oggetto ex conterrà al suo interno sia la versione client che l'sdk stava tentando di aggiornare che la versione ora presente dal server (e restituita dalla risposta HTTP/1.1 412 Precondition Failed).
Analizzando queste due versioni lo sviluppatore dell'applicazione sarà in grado di decidere cosa fare. Questo si esplicita nel far vincere la versione del server, far vincere quella del client, o anche eseguire un merge tra le due versioni e reinoltrare le modifche così ottenute di nuovo al server.
Forse non è molto noto che eseguendo un PullAsync implicitamente prima si esegue sempre un PushAsync, per cui situazioni come quella sopra devono essere gestite in entrambi I contesti.
Ritengo personalmente un grave errore non gestire situazioni di conflitto di modifiche concorrenti: infatti se la consistenza dei dati è distribuita su più tabelle correlate tra loro, non gestire correttamente i vari conflitti che possono avvenire può portare a incosistenze e problemi anche di natura sporadica.
Spero che queste poche note Vi siano utili.
Linkografia
Accessing optimistic concurrency features in Azure Mobile Services client SDKs
Managing Concurrency in Microsoft Azure Storage
Stack Overflow: Azure Table Storage - Managing Concurrency