Optimistic concurrency in xConnect
xConnect implements an optimistic concurrency model. This means that contacts and facets are not locked when they are read, and clients must anticipate concurrency conflicts when submitting changes. Calculated facets are automatically retried by the xConnect service layer.
The following steps describe what happens during a concurrency conflict:
-
When a contact or facet is created, a concurrency token is generated for that record by the storage provider.
-
When a contact or facet is requested, the concurrency token is included in the response (all contacts and facets have a
ConcurrencyToken
property). -
When a contact or facet is modified and saved, the incoming record is compared to the record in storage. If the concurrency tokens are different, it means that the record has been updated by another source since it was initially retrieved. In this case, xConnect will throw an exception and return the facet that is currently in storage.
-
At this point, you can choose whether or not to retry.
An optimistic concurrency model means that you cannot guarantee that an update will succeed. You must handle potential concurrency conflicts.
Reducing the risk of conflict
To reduce the risk of conflicts, it is recommended that you submit updates as soon as possible after retrieving a contact or facet. For example, Sitecore’s web tracker does not allow facets to be updated in session - you must submit updates to xConnect immediately using the xConnect Client API. Consider the following factors when assessing the risk of conflict:
-
How many systems are able to update a particular facet or contact
-
How often is a particular facet or identifier likely to be updated
Retrying conflicted operations
xConnect will throw an exception if one or more operations fail for any reason. If an operation fails due to a conflict, you can:
-
Automatically retry failed operations without prompting end user
-
Allow end user to choose conflict resolution behavior before submitting to xConnect, then automatically retry failed operations (for example, during a bulk import)
-
Prompt end user to resolve each conflict manually
-
Do nothing
Successful operations in the batch are not rolled back if one or more operations in the batch fail.
The following example demonstrates how to get and retry all operations that failed due to a conflict:
using Sitecore.XConnect.Collection.Model;
using Sitecore.XConnect;
using System;
using System.Threading.Tasks;
using System.Linq;
using Sitecore.XConnect.Operations;
using Sitecore.XConnect.Client;
namespace Documentation
{
public class RetryOperation
{
public async void ExampleAsync()
{
using (Sitecore.XConnect.Client.XConnectClient client = Sitecore.XConnect.Client.Configuration.SitecoreXConnectClientConfiguration.GetClient())
{
try
{
// Get contact
var reference = new Sitecore.XConnect.ContactReference(Guid.Parse("B9814105-1F45-E611-82E6-34E6D7117DCB"));
Task<Sitecore.XConnect.Contact> contactTask = client.GetAsync<Sitecore.XConnect.Contact>(reference, new Sitecore.XConnect.ContactExpandOptions(PhoneNumberList.DefaultFacetKey)
{
});
Sitecore.XConnect.Contact contact = await contactTask;
// Update some properties on an existing facet
PhoneNumberList phoneNumberList = contact.PhoneNumbers();
phoneNumberList.PreferredKey = "Work";
phoneNumberList.PreferredPhoneNumber = new PhoneNumber("44", "5555555");
// Set updated facet
client.SetPhoneNumbers(contact, phoneNumberList);
// Submit changes
await client.SubmitAsync();
}
catch (XdbExecutionException ex)
{
// Phone numbers changed since the facet was retrieved - a conflict error is thrown
// Get all operations where a conflict occurred - you must do this per operation type
var setPhoneOperations = ex.GetOperations(client).OfType<SetFacetOperation<PhoneNumberList>>()
.Where(x => x.Result.Status == SaveResultStatus.Conflict);
// Cycle through each operation that set a PhoneNumberList facet
foreach (var q in setPhoneOperations)
{
PhoneNumberList yourFacetValue = q.Facet; // Facet values that you tried to save - sync token is OUT OF DATE
PhoneNumberList currentFacetValue = q.Result.CurrentVersion; // Current value returned by xConnect - sync token is CORRECT
// At this point you can either resolve conflicts automatically OR present the end user
// with a message. In this scenario, we have decided to overwrite the facet values that were returend
// by xConnect with our own values. It is entirely up to you what the logic should be.
currentFacetValue.PreferredKey = yourFacetValue.PreferredKey;
currentFacetValue.PreferredPhoneNumber = yourFacetValue.PreferredPhoneNumber;
// Set the facet values again - note that if we passed in yourFacetValue, which has an out of date sync token
// the operation would fail. If the facet has changed AGAIN since it was retrieved, the operation will fail and you must
// choose whether to retry again.
client.SetFacet<PhoneNumberList>(q.FacetReference, currentFacetValue);
await client.SubmitAsync();
}
}
}
}
public void ExampleSync()
{
using (Sitecore.XConnect.Client.XConnectClient client = Sitecore.XConnect.Client.Configuration.SitecoreXConnectClientConfiguration.GetClient())
{
var preferredKey = "Work";
var preferredPhoneNumber = new PhoneNumber("44", "5555555");
try
{
// Get contact
var reference = new Sitecore.XConnect.ContactReference(Guid.Parse("B9814105-1F45-E611-82E6-34E6D7117DCB"));
Sitecore.XConnect.Contact contact = client.Get<Sitecore.XConnect.Contact>(
reference,
new ContactExpandOptions(PhoneNumberList.DefaultFacetKey)
{
});
// Update some properties on an existing facet
PhoneNumberList phoneNumberList = contact.PhoneNumbers();
phoneNumberList.PreferredKey = preferredKey;
phoneNumberList.PreferredPhoneNumber = preferredPhoneNumber;
// Set updated facet
client.SetPhoneNumbers(contact, phoneNumberList);
// Submit changes
client.Submit();
}
catch (AggregateException ex)
{
// Phone numbers changed since the facet was retrieved - a conflict error is thrown
// Get all operations where a conflict occurred - you must do this per operation type
foreach (var e in ex.Flatten().InnerExceptions)
{
if (e is FacetOperationException exception && exception.Result == SaveResultStatus.Conflict)
{
// At this point you can either resolve conflicts automatically OR present the end user
// with a message. In this scenario, we have decided to overwrite the facet values that were returned
// by xConnect with our own values. It is entirely up to you what the logic should be.
// Set the facet values again.
// If the facet has changed AGAIN since it was retrieved, the operation will fail and you must
// choose whether to retry again.
var entityId = exception.EntityId;
var facetKey = exception.FacetKey;
Sitecore.XConnect.Contact contact = client.Get<Sitecore.XConnect.Contact>(
new ContactReference((Guid)entityId),
new ContactExpandOptions(facetKey));
PhoneNumberList phoneNumberList = contact.PhoneNumbers();
phoneNumberList.PreferredKey = preferredKey;
phoneNumberList.PreferredPhoneNumber = preferredPhoneNumber;
client.SetFacet<PhoneNumberList>(contact, phoneNumberList);
client.Submit();
}
}
}
}
}
}
}
There is no generic way to resolve a conflict - you must account for each operation type and facet type that might result in a conflict.
.Facet versus .Result.CurrentVersion
The .Facet
property on each SetFacetOperation
object represents the facet object that you tried to submit. The .Result.CurrentVersion
property represents the most current facet returned by xConnect. You can use the returned facet to prompt the end user to choose a correct value or map your changes to .Result.CurrentVersion
and retry without prompting the end user.
Do not try to resubmit .Facet
- the sync token is out of date. You should map any properties to the .Result.CurrentVersion
object and submit that instead.