Handling Data Conflicts
Description — Couchbase Lite JavaScript — Handling conflict between data changes
Related Content — Remote Sync Gateway | CORS Configuration
Causes of Conflicts
Document conflicts can occur if multiple changes are made to the same version of a document by multiple peers in a distributed system. For Couchbase Mobile, this can be a Couchbase Lite or Sync Gateway database instance.
Such conflicts can occur after either of the following events:
-
A replication saves a document change — in which case the change with the most-revisions wins (unless one change is a delete). See Case 1: Conflicts when a replication is in progress
-
An application saves a document change directly to a database instance — in which case, last write wins, unless one change is a delete — see Case 2: Conflicts when saving a document
| Deletes always win. So, in either of the above cases, if one of the changes was a Delete then that change wins. |
The following sections discuss each scenario in more detail.
|
Dive deeper …
Read more about Document Conflicts and Automatic Conflict Resolution in Couchbase Mobile.
|
Conflicts when Replicating
There’s no practical way to prevent a conflict when incompatible changes to a document are made in multiple instances of an app. The conflict is realized only when replication propagates the incompatible changes to each other.
-
Alice uses her device to create DocumentA.
-
Replication syncs DocumentA to Bob’s device.
-
Alice uses her device to apply ChangeX to DocumentA.
-
Bob uses his device to make a different change, ChangeY, to DocumentA.
-
Replication syncs ChangeY to Alice’s device.
This device already has ChangeX putting the local document in conflict.
-
Replication syncs ChangeX to Bob’s device.
This device already has ChangeY and now Bob’s local document is in conflict.
Automatic Conflict Resolution
| The rules only apply to conflicts caused by replication. Conflict resolution takes place exclusively during pull replication, while push replication remains unaffected. |
Couchbase Lite uses the following rules to handle conflicts such as those described in Example 1:
-
If one of the changes is a deletion:
A deleted document (that is, a tombstone) always wins over a document update.
-
If both changes are document changes:
The change with the most revisions wins. If both have the same number of revisions, a deterministic algorithm is used to pick a winner.
The result is saved internally by the Couchbase Lite replicator. Those rules describe the internal behavior of the replicator. For additional control over the handling of conflicts, including when a replication is in progress, see Custom Conflict Resolution.
Custom Conflict Resolution
Application developers who want more control over how document conflicts are handled can use custom logic to select the winner between conflicting revisions of a document.
If a custom conflict resolver is not provided, the system will automatically resolve conflicts as discussed in Automatic Conflict Resolution.
| Custom conflict handlers should be optimized and fast. Time-consuming conflict resolution can slow down the replication process significantly. |
To implement custom conflict resolution during replication, you create a conflict resolver function and configure it on the replicator.
Conflict Resolver
Apps have the following strategies for resolving conflicts:
-
Local Wins: The current revision in the database wins.
-
Remote Wins: The revision pulled from the remote endpoint through replication wins.
-
Merge: Merge the content of the conflicting revisions.
-
Local Wins
-
Remote Wins
-
Merge
-
Delete
const localWinsResolver: PullConflictResolver = async (local, remote) => {
return local;
};
const remoteWinsResolver: PullConflictResolver = async (local, remote) => {
return remote;
};
const mergeResolver: PullConflictResolver = async (local, remote) => {
if (local && remote) {
return { ...local, ...remote } as CBLDocument;
} else {
return local ?? remote;
}
};
const deleteResolver: PullConflictResolver = async (local, remote) => {
return null;
};
When null is returned by the resolver, the conflict is resolved as a document deletion.
Conflicts when Saving
When updating a document, you need to consider the possibility of update conflicts. Update conflicts can occur when you try to update a document that’s been updated since you read it.
Here’s a typical sequence of events that would create an update conflict:
-
Your code reads the document’s current properties, and constructs a modified copy to save.
-
Another thread (perhaps the replicator) updates the document, creating a new revision with different properties.
-
Your code updates the document with its modified properties using the save operation.
Automatic Conflict Resolution
In Couchbase Lite, by default, the conflict is automatically resolved and only one document update is stored in the database. The Last-Write-Win (LWW) algorithm is used to pick the winning update. So in effect, the changes from step 2 would be overwritten and lost.
If the probability of update conflicts is high in your app and you wish to avoid the possibility of overwritten data, you can use a custom save handler with concurrency control.
Save with Conflict Handler
Implement a conflict handler when saving documents to handle conflicts during save operations:
// Use my changes
await tasks.save(doc, (_mine, _theirs /* conflicting */) => {
return 'replace';
});
// Discard my change
await tasks.save(doc, (_mine, _theirs /* conflicting */) => {
return 'revert';
});
// Abort with an error
await tasks.save(doc, (_mine, _theirs /* conflicting */) => {
return 'fail';
});
The conflict handler receives:
-
document- The document being saved -
conflicting- The current document in the database (that conflicts)
The handler should return:
-
The resolved document to save
-
nullto cancel the save operation
Custom Merge on Save
Implement property-level merging when saving:
// Use my changes
await tasks.save(doc, (mine, theirs /* conflicting */) => {
// Delete always wins
if (!theirs) return 'revert';
// Custom merge
mine.title = `${mine.title} and ${theirs.title}`;
mine.completed = !!(mine.completed && theirs.completed);
mine.priority = Math.min(mine.priority, theirs.priority);
mine.createdAt = new Date().toISOString();
return 'replace';
});