Conflict Handling
Mode is Immutable, conflict handling must be defined during container creation.
Only for multi-region write
If there are multi-region write, we need to consider custom handling of overrides on the same data.
Few methods for conflict handling are:
- Use custom conflict with stored procedure, (register in container before create). You need to define a custom field like _etag to compare, or a numeric field.
- Use last writer wins that is based on timestamp, so no headache.
- last writer wins with custom field. Field must be in a numeric format (e.g., epoch Date/Time).
- Conflict Feed - Manual (you set customResolutionPolicy to Custom, but empty stored procedure). Need to call via a trigger to execute container.Conflicts.GetConflictQueryIterator to find all the conflicts. If this method is not called we will waste storage space.
Types of Conflict Resolution
- Last Writer Wins
- default _ts field
- Custom epoch field
- Custom
- Custom with Stored Procedure
- Custom with No Stored Procedure / Manual (aka.Conflict Feed)
Last Writer Wins
- Either a field to use _ts
- Defines a field to compare and must be numeric (if fail will use _ts)
- If you are using "Last Writer Wins," you can actually update the Path (e.g., changing it from /_ts to /myCustomId) on the fly. What you CANNOT change: You cannot toggle to Custom Mode.
DocumentCollection lwwCollection = await createClient.CreateDocumentCollectionIfNotExistsAsync(UriFactory.CreateDatabaseUri(this.databaseName), new DocumentCollection
{
Id = this.lwwCollectionName,
ConflictResolutionPolicy = new ConflictResolutionPolicy {
Mode = ConflictResolutionMode.LastWriterWins,
ConflictResolutionPath = "/myCustomId",
},
});
Custom with Stored Procedure
To handle conflict via stored procedure to be called. E.g. 2 records update has an issue. https://learn.microsoft.com/en-sg/training/modules/configure-multi-region-write-azure-cosmos-db-sql-api/5-create-custom-conflict-resolution-policy
Steps:
- Stored Procedure Registration: You must then register (create) the actual JavaScript stored procedure on that container using the SDK or the Azure Portal.
- Defined the conflict feed when creating the container.
SDK Bicep AZ CLI
"conflictResolutionPolicy": {
"mode": "Custom",
"conflictResolutionProcedure": "sprocs/yourMergeProcedureName"
}
Database database = client.GetDatabase(databaseName);
ContainerProperties properties = new(containerName, partitionKey)
{
ConflictResolutionPolicy = new ConflictResolutionPolicy()
{
Mode = ConflictResolutionMode.Custom,
ResolutionProcedure = $"dbs/{databaseName}/colls/{containerName}/sprocs/{sprocName}",
}
};
Container container = database.CreateContainerIfNotExistsAsync(properties);
Custom with No Stored Procedure / Manual
- Writes and stores into "conflict feed", that needs to be manually read.
- Consumes storage and RU and stores "invisible" in another conflict feed tab. It handles restarts and needs to be manually cleared.
- Last Writer Wins with _etag executes first. As a result, inconsistent data can occur before the feed is processed.
- Conflict data has to be MANUALLY deleted.
| Feature | Conflict Feed | Regular Container |
|---|---|---|
| Storage Type | Persistent SSD (Cloud) | Persistent SSD (Cloud) |
| Visibility | Sub-resource of a container | Main database resource |
| Lifecycle | Stays until manually cleared | Stays until deleted/TTL |
| Cost | Consumes storage & RU (for reads) | Consumes storage & RU |
ContainerProperties containerProperties = new ContainerProperties("myContainer", "/partitionKey")
{
ConflictResolutionPolicy = new ConflictResolutionPolicy()
{
Mode = ConflictResolutionMode.Custom
# No ResolutionProcedure Defined.
}
};
public async Task ResolveConflictsAsync(Container container)
{
// 1. Get an iterator for the conflicts feed
using FeedIterator<ConflictProperties> conflictIterator = container.Conflicts.GetConflictQueryIterator<ConflictProperties>();
while (conflictIterator.HasMoreResults)
{
foreach (ConflictProperties conflict in await conflictIterator.ReadNextAsync())
{
// 2. Read the "Incoming" item (the one that was rejected)
MyDataType incomingItem = await container.Conflicts.ReadConflictContentAsync<MyDataType>(conflict);
// 3. Read the "Current" item (the one that currently exists in the container)
ItemResponse<MyDataType> currentResponse = await container.ReadItemAsync<MyDataType>(
conflict.Id,
new PartitionKey(incomingItem.PartitionKey));
MyDataType currentItem = currentResponse.Resource;
// 4. YOUR CUSTOM LOGIC: Decide who wins
// Example: Merge their arrays or pick the one with the most recent 'lastUpdated'
MyDataType winner = MyBusinessLogic.Merge(incomingItem, currentItem);
// 5. Update the container with the winner
await container.UpsertItemAsync(winner, new PartitionKey(winner.PartitionKey));
// 6. DELETE the conflict from the feed (otherwise it stays there forever)
await container.Conflicts.DeleteAsync(conflict, new PartitionKey(incomingItem.PartitionKey));
Console.WriteLine($"Resolved conflict for ID: {conflict.Id}");
}
}
}
| Feature | Custom (Stored Procedure) | Manual (Conflict Feed) |
|---|---|---|
| Language | JavaScript (inside Cosmos DB) | Any SDK language (C#, Java, etc.) |
| Execution | Synchronous: Happens during the write. | Asynchronous: Happens after the conflict. |
| Complexity | Limited by Stored Proc constraints. | Unlimited (can call external APIs/DBs). |
| Latency | Immediate resolution. | Resolution is delayed until your app reads the feed. |
| Debugging | Difficult (DB logs). | Easy (Standard app debugging). |
Container Policy Configuration
When you create the container (it must be set at creation time, it cannot be changed later), you must set the ConflictResolutionMode property to Custom and set the ResolutionProcedure property to the name of your merge stored procedure. - Azure Cosmos DB doesn't directly support altering the conflict resolution policy after the container has been created. - The policy is set at container creation and remains immutable. Trying to change it directly would require recreating the container. - Can be created via ARM/Portal/SDK
Extra
| Type | Description |
|---|---|
| Insert | This conflict occurs when more than one item is inserted simultaneously with the same unique identifier in multiple regions |
| Replace | Replace conflicts occur when multiple client applications update the same item concurrently in separate regions |
| Delete | Delete conflicts occurs when a client is attempting to update an item that has been deleted in another region at the same time |
| Parameter | Description |
|---|---|
| existingItem | The item that is already committed |
| incomingItem | The item that's being inserted or updated that generated the conflict |
| isTombstone | Boolean indicating if the incoming item was previously deleted |
| conflictingItems | Array of all committed items in the container that conflict with incomingItem |
Food for thought
You can use Custom conflict policy with stored procedure that examines remoteRegionId and you can prioritise region.