...ovvero la gestione della concorrenza, intesa non come trattare con un'azienda che fa il nostro stesso mestiere ma come "modifiche concorrenti che avvengono sullo stesso dato".

La problematica in analisi nasce dalla seguente situazione. Si immagini il caso in cui in cui la stessa versione di un post all'interno della nostra fantastica app Focac-Book venga modificato da due dispositivi diversi: quale delle due modifiche dovrebbe vincere ??

Per meglio comprendere penso sia utile scendere più nel dettaglio.

Due dispositivi contemporanemente scaricano lo stesso set di dati, che quindi viene mantenuto nella memoria volatile di ciascun dispositivo (la versione della app che stiamo analizzando NON ha ancora l'offline-sync, per cui i dati scaricati rimangono nella memoria volatile di ciscun dispositivo).

In questa condizione guardacaso entrambi i dispositivi modificano lo stesso record, che al salvataggio viene quindi sottoposto al backend.

Questo vuole dire che entrambi gli utilizzatori aprono in modifica lo stesso record, e sui dati presentati (che come detto sono i medesimi) decidono le modifiche da apporvi.

Al salvataggio sul backend quale delle due modiifche dovrebbe vincere ? Il record che è stato oggetto di modifiche è il medesimo ! E un utilizzatore ha fatto delle modifiche basadosi sul valore attuale assunto dal record.

Una prima risposta potrebbe essere che la modifica sottoposta per ultima al backend vince su tutto, e quindi sovrascrive eventuali altre modifiche dall'altro dispositivo.

A pensarci bene, però, questo potrebbe non essere sempre il comportamento desiderato.

La cosa migliore per gestire un caso come quello presentato sopra, e che trasfomerebbe la nostra app un a "cosa strafica", sarebbe presentare all'utilizzatore che per ultimo sottopone le modifiche una maschera che lo avvisi di quanto occorso, magari proponendogli i valori della modifica appena accetta (ed eseguita dall'altro utilizzatore) in confronto ai valori da lui modificati, così da permettergli di scegliere quale versione mantenere.

Semplice a dirsi, il flusso del funzionamento direi che è abbastanza chiaro, ma realizzare una cosa del genere può diventare un un pò complicato.

Ecco però che fantasticamente l’SDK in studio in questa serie ci permette di gestire questa situazione in modo semplice e intuitivo !

Prima di addentrarci nel codice occorre dire che la concorrenza nell'SDK viene gestita con l'ausilio del tipo colonna timestamp di Sql Server, a cui viene assegnato il nome version (vedere modelli delle puntate precedenti).

Ma cos'è stà colonna di tipo timestamp ?

Ogni database presente in Sql Server ha associato un contatore che viene incrementato ad ogni operazione di insert o update su una qualsiasi delle tabelle che lo compongono.

Indicando su una tabella qualsiasi un campo di tipo timestamp si vuole dire a Sql Server: ... Hey, dopo aver fatto la modifica o l'inserimento di una nuova riga, e aver aggiornato il contatore di cui sopra, poni su questa colonna il valore attuale assunto da questo contatore.

In altri termini ad ogni operazione su di una riga di una tabelle di inserimento o modifica il campo di tipo timestamp sarà automaticamente popolato con tale valore al momento dell'operazione.

Così facendo la colonna che ospita il timestamp può essere immaginata come una valore che contiene una versione del dato contenuto sulla riga stessa.

Oss.: Una tabella può ospitare al massimo una colonna di tipo timestamp.

Quindi come può aiutare la colonna di tipo timestamp nella gestione della concorrenza ?

 

Step01

Il dispositivo A prende il set di dati da Sql Server usando il backend. La riga i-esima ha il valore version pari a x.

Step 02

Il dispositivo B prende lo stesso set di dati e quindi anche per lui la riga i-esima avrà nella colonna version il valore x

Step 03

Il dispositivo A modiifca la riga i-esima e sottpone la modifica al backend per la persistenza su Sql Server. La modifica sul tablet non ha in alcun modo intaccato il valore della colonna version, pertanto la riga i-esima sottoposta al backend avrà i valori aggiornati ma il valore della colonna version sempre pari a x. Il backend non vede nulla di strano e sottopone la modifica a Sql Server, che rende persistenti le modifiche eseguite dal tablet A e quindi la colonna version ora assumerà il valore y.

Step 04

Il dispositivo B esegue, mannaggia  lui, anch'esso una modifca alla riga i-esima. Anche in questo caso la modifica viene sottoposta al backend: questa volta però viene verificato che il valore assunto dalla colonna version ha anche valore x, mentre nel database la stessa colonna ha valore y. Ecco che quindi c'è stato un problema di concorrenza.

Le gestione della concorrenza sopra viene chiamata Optimistic Concurrency. Ve lo dico se volete sfoggiare il termine con i Vs amivi al bar durante l'aperitivo.

Il sistema esposto sopra funziona in modo perfetto: ovviamente i più lagnosi di Voi potrebbero osservare che senza scomodare la colonna version si sarebbe potuto confrontare direttamente il valore assunto da ogni colonna. Vero, anzi verissimo, però ovviamente risulta essere più veloce ed efficace il confronto su un'unica colonna con il metodo esposto sopra.

Poi volete mettere il divertimento (?) di usare una colonna timestamp invece che un mero confronto di valori ? Noi sì che sappiamo divertirci, nevvero ?!

Quindi per tornare a bomba la colonn version è utilizzata dall'SDK per comprendere il verificarsi di una modifica concorrente. Quindi se il valore della colonna version viene viene incluso nel modello posto lato app, vuole dire che si vuole gestire la concorrenza usando il ora esposto.

Se invece tale valore semplicemente NON viene incluso, allora vincerà sempre l'ultima modifica.

Siccome noi non vogliamo mai stare tranquilli, vogliamo avere la concorrenza strafica: per tale motivo occorre includere nel modello lato client la colonna version.

public class FocaccePost
{
  public string Id { get; set; }
  
  [Microsoft.WindowsAzure.MobileServices.Version]
  public byte[] Version { get; set; }
  
  public string NomeUtente { get; set; }

  public string Luogo { get; set; }

  public DateTime DataOra { get; set; }

  public int Voto { get; set; }
}

Aggiungere semplicemente la proprietà al modello non è sufficiente. Occorre mettere anche mano alla gestione degli errori del metodo delegato a salvare i dati.

public async Task AddUpdateItemAsync(FocaccePost focaccePost)
{
	IMobileServiceTable Focacciatable = client.GetTable();
	try
	{
		if (string.IsNullOrEmpty(focaccePost.Id))
		{
			//e' un inserimento
			await Focacciatable.InsertAsync(focaccePost);
		}
		else
		{
			//è una modifica
			await Focacciatable.UpdateAsync(focaccePost);
		}
	}

	//Gestione concorrenza
	catch (MobileServicePreconditionFailedException conflict)
	{
		FocaccePost versioneserver = conflict.Item;
		//Qui si può decidere che fare

		//Vince il server
		//Non devo fare nulla


		//Vince il client: faccio quanto sotto, cioè assegno a version il valore prelevato dal server
		focaccePost.Version = versioneserver.Version;
		await Focacciatable.UpdateAsync(focaccePost);
	}

	catch (Exception e)
	{
		throw;
	}
}

Per inciso: sul backend non deve essere fatta alcuna modifica: basta modificare il valore .

Quando si usa il metodo UpdateAsync il dato aggiornato verrà inviato al backend, che prima di salvare farà la verifica della colonna version: se la versione inviata dalla app non è la stessa presente sul database verrà provocato sul metodo un errore precondition failed 412, che viene furbescamente "trappato" e gestito all'interno dell'eccezione evidenziata, cioè catch (MobileServicePreconditionFailedException conflict).

Qui si ha a disposizione la versione che si stava tentando di salvare ed è possibile ricavare da conflict.Item la versione che è invece salvata lato server.

A questo punto è possibile far vedere all'utilizzatore una maschera presentando i valori, o anche implementare logiche che permettano di capire quale versione tenere.

Al termine è possibile con poco sforzo far vincere una versione (quella locale) o l'altra (quella sul database), o magari è anche possibile fare un merge delle due situazioni.

Direi che il codice presentato è abbastanza esplicativo per comprendere come gestire il.... the winner is.....

Nota per gli smanettoni: per provare la concorrenza con il codice sopra basta prendere in modifica una riga e prima di salvarla da un altro dispositivo, o anche usando ad esempio SSMS, fare una modifica sulla stessa riga e salvarla: solo allora procedere al salvataggio finale. Questo provocherà l'errore di cui sopra.

Spero che questa puntata Vi sia stata utile e non sia risultata troppo noiosa. Alla prossima !!!

Linkografia

Git Page: Focac-Book in Xamarin