Why Can't We Be Friends? Conflict Resolution in Azure Mobile Services
The story is always the same… we develop this brand new app, introduce it to Azure, and they get along like gangbusters… constantly sending data back and forth. Then they have a little squabble, the lights go out and the internet is down, but Azure & the app survive with some offline data sync’ing.
But then it happens… a major conflict rears its ugly head testing the friendship between our app and Azure! Well, we’re not going to let a little road bump ruin a good thing! We’re going to show these two a little Conflict Resolution!
What Kind of Conflicts Can Occur?
It’s a rough world out there, lots of different ways to have a misunderstanding and get into a conflict, especially when our app can travel all over the place, go offline and then sync data back up later… so let’s walk through some of the scenarios that lead to conflicts so we can help our app and Azure before their spat turns into an all out fight!
And there are many, many more different ways conflicts can arise. I also already know what you’re thinking … wow, this is really going to be a pain to write all the code to detect the various conflicts that arise, much less handle them all.
The good news is that we can handle them all – and the even better news is that a lot of the time we don’t even need to detect them on our own!
In the next sections we’ll cover how Azure automatically detects conflicts. We’ll talk about how to handle these conflicts in our Xamarin Forms (or classic Xamarin) apps when using a regular, always-on Azure connection, as well as the offline sync connection. We’ll also talk about some design considerations of where the conflict exception handling should take place.
Azure Auto-Conflict Detection
Azure provides automatic conflict detection in cases where a record within the backing Azure data store has been modified in some way (including being deleted) before the modified record from our app is received.
Going back the Cheesed app that we’re slowing building along with these Azure series of posts, an example would be if somebody updated the address of a dairy while offline, but found that particular dairy was already updated when the offline data was pushed.
The Azure Mobile Services component will throw an exception when this happens.
The way Azure accomplishes this is called Optimistic Concurrency Control. Essentially what that means is Azure assumes every transaction it receives can be committed, so it does not lock any record at any time. Rather, it updates and changes the _version column upon every write operation. (The _version column is more or less a timestamp column).
If the _version column of the incoming record does not match that of what is stored on the server, Azure determines the incoming record is in conflict with what is on the server and returns an HTTP error – which renders itself as an exception within the Azure Mobile Services component in our app.
So what do we need to do in order to enable Optimistic Concurrency Control within Azure and our app? We just need to make sure our model class has the _version column mapped … that’s it – everything else will be taken care of for us!
public class Dairy
{
public string Id {
get;
set;
}
[JsonProperty("address")]
public string Address {
get;
set;
}
[JsonProperty("dairyname")]
public string DairyName {
get;
set;
}
[JsonProperty("affineur")]
public string CheeseMaker {
get;
set;
}
// Being explicit that this is our version column
[Microsoft.WindowsAzure.MobileServices.Version]
public byte[] Version {
get;
set;
}
}
To handle any conflicts that come back we need to handle one of two exceptions, depending on whether we’re using offline sync or just pushing to Azure without offline capabilities.
MobileServicePushFailedException
needs to be handled when the app is performing pushes during offline syncs AND a customIMobileServiceSyncHandler
is NOT implemented on the SyncContext (more on this below).MobileServicePreconditionFailedException
needs to be handled when the app is not offline enabled or when a customIMobileServiceSyncHandler
is implemented on the SyncContext.
Handling The Conflict In The App
OK … Azure has done its part and told the app that there’s a problem. But conflict resolution is a two way street, and we’re going to have to handle the conflict within the app in order to resolve this.
What To Do When a Conflict Is Detected?
That’s the million dollar question isn’t it? There are multiple things that can be done, and they’re all dictated by the business rules of your application.
For example…
- Last update always wins.
- Give the user a screen & have them select either the local version or the server version to win.
- Have the user perform a merge, picking and choosing fields from each version and creating a new hybrid record to update.
- Just cancel the modification altogether.
Don’t handle the exception and let the app crash.Err … wait, that’s not really acceptable!
Regardless of how the business rules dictate the resolution to the conflict, you first have to handle the conflict exception efficiently within the app, invoke the resolution mechanism, then try the push operation again.
Handling Push Conflicts
Here we’re talking about pushing data to Azure without offline sync services being involved. We’ll have to handle the conflict inline with the code when we invoke the push up to Azure.
Azure Mobile Services will throw the MobileServicePreconditionFailedException
when the _version column of the incoming record does not match what is on the server. Here’s a simple implementation of catching, handling and resolving this conflict:
private async Task UpdateDairy (Dairy updatedDairy)
{
MobileServicePreconditionFailedException<Dairy> preconditionFailure = null;
try {
var onlineDairyTable = App.MobileService.GetTable<Dairy> ();
await onlineDairyTable.UpdateAsync (updatedDairy);
} catch (MobileServicePreconditionFailedException<Dairy> ex) {
preconditionFailure = ex;
}
// Oh no - a conflict!
if (preconditionFailure != null) {
// Note that the "Item" property is a copy of the dairy row from the server that's trying to be updated
await ResolveConflict (updatedDairy, preconditionFailure.Item);
}
}
private async Task ResolveConflict (Dairy localVersionOfDairy, Dairy azuresVersionOfDairy)
{
// Here we're just deciding to use our app's version of the dairy by
// giving it the same _version column value of what is in the server - (last in wins)
localVersionOfDairy.Version = azuresVersionOfDairy.Version;
await UpdateDairy (localVersionOfDairy);
}
There is a property in MobileServicePreconditionFailedException
called Item
which has the server’s version of the record that is trying to be updated. By using that along with the local copy, we should be able to resolve the conflict in whatever way our business rules dictate. (In the example above, the last update always wins).
The one thing to make sure you do is set the _version column in the “resolved” object that will be pushed to Azure to be the same as what the server’s copy is. That way Azure will not mark it as a conflict again (unless another update happened during our resolution!)
Handling Offline Sync Push Conflicts – Inline
While you can handle conflicts that arise from sync’ing data from an offline data store inline as illustrated above, it comes with several downsides, and I would say it’s, in general, not the greatest idea.
To get a handle on why – just think about how pushing works in Azure – all of the pending changes go at once. That means the error(s), and there could be more than one, come back at once. Kind of a pain to implement a handler, because most likely you’re going to have more than one place where Pushes, Pulls and Purges happen from.
If you insist to handle them inline from a Push, Pull or Purge operation on your SyncContext, you can catch the MobileServicePushFailedException
.
So … instead of handling all of the conflicts sent by the offline sync context at once, in a big jumbled mess, wouldn’t it be better if we could handle them one by one, in single spot, as they get returned by Azure?
Handling Offline Sync Push Conflicts – Centralized
Alright, now that we’re convinced that catching possible conflicts after each time we invoke a Push, Pull or Purge operation in our code isn’t the greatest idea in the world, where would a good place be?
If you remember back to the previous post on offline editing and syncing, when we setup our SyncContext, we have to pass it an implementation of the IMobileServiceLocalStore
– or something to hold the data locally. And optionally we can also pass it an implementation of IMobileServiceSyncHandler
.
That interface allows us to define 2 functions, one to “intercept” the outgoing requests ExecuteTableOperationAsync
to Azure and the other handle the reply from Azure OnPushCompleteAsync
.
The first instinct would be to handle the conflict errors in the OnPushCompleteAsync
function – but that’s only invoked after all of the push operations are finished – thus all of the conflicts that occur will be included. So we’re not much better off than we were in the inline exception handling.
Rather, we want to check for conflicts in the ExecuteTableOperationAsync
function. This function will get invoked for each and every pending table operation queued up in local data store.
A very simple implementation of this function may look like this:
public async Task<JObject> ExecuteTableOperationAsync (IMobileServiceTableOperation operation)
{
JObject result = null;
MobileServicePreconditionFailedException<Dairy> dairyConflict = null;
MobileServicePreconditionFailedException<Cheese> cheeseConflict = null;
try {
operation.AbortPush();
result = await operation.ExecuteAsync ();
} catch (MobileServicePreconditionFailedException<Dairy> ex) {
// Azure's version of the Dairy - Strongly typed
var serverDairyItem = ex.Item;
// App's version of the Dairy - in conflict with what is in Azure
var localDairyItem = operation.Item.ToObject<Dairy> ();
// Do something to handle the dairy conflict
} catch (MobileServicePreconditionFailedException<Cheese> ex) {
// Azure's version of the Cheese - strongly typed
var serverCheeseItem = ex.Item;
// App's version of the Cheese - in conflict with what is in Azure
var localCheeseItem = operation.Item.ToObject<Cheese>();
// Do something to handle the cheese conflict
}
return result;
}
It’s also within this function that we can decide to cancel the table operation for some reason by invoking the AbortPush()
function on the IMobileServiceTableOperation
variable being passed in.
One thing worth noting here is that invoking AbortPush()
will not remove the pending operation from the overall queue, it will just remove it from the current push. You can make any changes to that record you’d like, and that updated record will be in the next push.
To totally remove a record from the pending operations, you need to invoke one of the various overloads for PurgeAsync()
on the sync context table and pass true
in for the “force” parameter. That will remove the record without invoking a push.
Summary
Like just about all conflicts in life, Azure conflict resolution really just comes down to communication. And we can help with the communication by the following:
- First Azure needs to have its Optimistic Concurrency Control turned on by having our model objects implement the _version column.
- Then we need to efficiently and properly handle the conflict exception when Azure throws one.
- Finally, we need to implement some business logic which resolves the conflict and eventually retry the Azure data operation.
All seems pretty easy when we put it that way… makes you wonder why Azure & our app got in a fight in the first place?