Ho già parlato nella quinta parte di questa serie della gestione della concorrenza, introducendo la teoria che sottende l'optimistc concorrency così come implementato dall'SDK Azure Mobile, ed esposto aluni codici pratici per gestire la problematica .

In questo momento, però, si sta parlando dell'offline-sync: in tale contesto vedremo che le cose, seppur simili a quanto visto in precedenza, sono giocoforza un pò più complicate.

Inziamo con il dire che per rilevare le modifiche concorrenti la tecnica dall'SDK è sempre la stessa: utilizzo della colonna version (...squadra vincente non si cambia, no ?).

Però nell'online-sync ogni singolo salvataggio viene sottoposto al backend, e quindi l'eventuale gestione della modifica concorrente avviene sul singolo record oggetto del salvataggio.

Nell'offline-sync, invece, a fronte di un singolo push (implicit o meno) si possono sottoporre al backend un numero imprecisato di record modificati, che magari insistono su più tabelle.

Per questo motivo in questo in questo caso potrebbe essere necessario gestire le modifiche concorrenti di più di un record oggetto dell'operazione di push.

Rammento che il push nell'offline-sync sottopone tutte le modifiche avvenute sul database locale SqLite al backend per la loro persistenza sulla base dati remota con lo stesso ordine con cui queste sono avvenute, e richiamando i vari metodi dei controller coinvolti, semplicemente invocando il metodo PushAsync dell'oggetto SyncContext.

Per questo motivo nel caso dell'offline-sync l'SDK Azure Mobile Client mette a disposizione una gestione più articolata della concorrenza, anche se i principi ispiratori sono assolutamente analoghi a quanto già visto.

Aggiungo anche che lavorare con offline-sync sicuramente introduce maggiori probabilità di avere modifiche concorrenti, e un'applicatvo reale dovrebbe poter gestire questa parte in modo preciso e dettagliato sopratutto quando sono coinvolte più tabelle che magari hanno relazioni semantiche tra esse.

Nel seguito un primo esempio di codice utilizzato per gestire i problemi relativi alla concorrenza.

 

public class AzureMobileClient
{
	MobileServiceClient client;

	public AzureMobileClient()
	{
		client = new MobileServiceClient(<URL servizio Azure>);
		
	}

	public async Task Initialize()
	{

		if (client?.SyncContext?.IsInitialized ?? false)
			return;


		string DbPath = Path.Combine(MobileServiceClient.DefaultDatabasePath, "focac-book.db");

		var store = new MobileServiceSQLiteStore(DbPath);

		//definisco la tabella
		store.DefineTable();

		//Initialize SyncContext
		await client.SyncContext.InitializeAsync(store);



	}

	public async Task<ICollection> ReadAllItemsAsync()
	{
		
		try
		{
			IMobileServiceSyncTable Focacciatable = client.GetSyncTable();
			await SyncFocacceDB();
			return await Focacciatable.ToListAsync();
		}
		catch (Exception e)
		{

			throw;
		}

	}


	public async Task SyncFocacceDB()
	{
		try
		{

			IMobileServiceSyncTable Focacciatable = client.GetSyncTable();
			await Focacciatable.PullAsync("allFocacce", Focacciatable.CreateQuery());


		}

		catch (MobileServicePushFailedException conflict)
		{
			if (conflict.PushResult != null)
			{
				foreach (var error in conflict.PushResult.Errors)
				{
					await ResolveConflictAsync(error);
				}
			}
		}

		catch (Exception ex)
		{
			Debug.WriteLine("Unable to sync focacce, that is alright as we have offline capabilities: " + ex);
		}

	}

	public async Task PushModifiche()
	{
		try
		{

			await client.SyncContext.PushAsync();

		}


		catch (MobileServicePushFailedException conflict)
		{
			if (conflict.PushResult != null)
			{
				foreach (var error in conflict.PushResult.Errors)
				{
					await ResolveConflictAsync(error);
				}
			}
		}

		catch (Exception ex)
		{
			Debug.WriteLine("Unable to sync focacce, that is alright as we have offline capabilities: " + ex);
		}

	}

	private async Task ResolveConflictAsync(MobileServiceTableOperationError error)
	{

		//error.Result è il valore presente nel backend
		if (error.OperationKind == MobileServiceTableOperationKind.Update && error.Result != null)
		{
			//error.Result -> Il record sul server che ha dato l'errore in formato oggetto Json
			//error.Item -> Il recod locale che ha dato provocato l'errore in formato oggetto Json


			//Vince il server !!!
			//Modifico la tupla locale con quanto ricevuto dal server
			await error.CancelAndUpdateItemAsync(error.Result);


			//Riforzo la tupla locale con i suoi stessi valori e le modifiche verranno riproposte al backend
			//await error.UpdateOperationAsync(error.Item);
		}
		else
		{
			//In alcuni casi (per esempio per proxy interposti o altri casi sfortunati) error.result è null
			await error.CancelAndDiscardItemAsync();
		}
	}

	public async Task AddUpdateItemAsync(FocaccePost focaccePost)
	{

		try
		{
			IMobileServiceSyncTable Focacciatable = client.GetSyncTable();
			if (string.IsNullOrEmpty(focaccePost.Id))
			{
				//e' un inserimento
			   
				await Focacciatable.InsertAsync(focaccePost);
			}
			else
			{
				//è una modifica
				await Focacciatable.UpdateAsync(focaccePost);
			}

			await PushModifiche();
		}

		catch (Exception e)
		{
			throw;
		}
	}
	public async Task DeleteItemAsync(FocaccePost focaccePost)
	{

		try
		{
			IMobileServiceSyncTable Focacciatable = client.GetSyncTable();
			await Focacciatable.DeleteAsync(focaccePost);



		}
		catch (Exception e)
		{

			throw;
		}

	}

}

A causa dell’implicit puh una innocua operazione di pull, potrebbe scatenare anche dei push delle modifiche ancora presenti sul database locale SqLite e non ancora riportate sul backend: per tale motivo occorre implementare una gestione della concorrenza non solo nel metodo PushModifiche, ma anche in SyncFocacceDB Async.

Nulla di esotico: i problemi relativi alla concorrenza vengono risolti nel metodo ResolveConflictAsync.

Comunque in definitiva è possibile gestire il caso di una modifica concorrente con una delle tre possibili azioni.

CancelAndUpdateItemAsync(JObject item)
Cancella l’aggiornamento in corso e aggiorna l’item locale incriminato con quanto passato come argomento di questo comando. Tutte le pending operation inerenti questo record vengono cancellate.

UpdateOperationAsync(JObject item)
Aggiorna il record locale con quanto passato come argomento di questo comando e quindi al prossimo push l'eventuale modiifca qui eseguita sarà riproposta al backend e sarà resa persistente anche su SQl Server (cioè la tabella delle pending operation per il record viene aggiornata con le modifiche qui eseguite con questo comando - nella CancelAndUpdateItemAsync le pending operation per il record venivano annullate).

CancelAndDiscardItemAsync()
Le modifiche sul record vengono cancellate e così anche i relativi record nella tabella delle pending operation. Tutte le pendig operation inerenti questo record vengono cancellate.

Nel codice dell'esempio viene data precedenza alle modifiche avvenute sul server: un app reale complessa dovrebbe tenere presente altre condizioni tenendo conto della semantica del dato e/o anche con l’ausilio di una maschera da far apparire all’utilizzatore dove si richiede come risolvere la "controversia".

Quindi mi permetto di ripetere: don't do this at home ! In una app con molti dati coinvolti e si suppone di livelllo professionale occorre gesitire le modifiche concorrenti in modo più articolato che come presentato nel codice di esempio.

Qualsiasi algoritmo utilizziate per risolvere la concorrenza tenete sempre presente che per mille ragioni il valore che dovrebbe rappresentare il valore assunto dal server può assumere il valore nulla, e pertnto è buona norma verificarlo.

Linkografia

Git Page: Focac-Book in Xamarin