Conflict Resolution with Business Logic
This tutorial shows one way to perform conflict resolution in a collection's
onPreSave
hook.
This tutorial assumes that you have
- Created a Kinvey app.
- Are familiar with a Kinvey client library.
- Know how to use the client library's custom request property functionality.
Code
The included code can be used for the majority of conflict resolution cases,
but you'll want to Customize your customEntityMerge
function.
function onPreSave(request, response, modules){
var conflictPropertyName = "ConflictResolutionWinner"; // The name of the custom property
var collectionName = "conflictCentral"; // The name of the collection we're in
// Kinvey module aliases
var l = modules.logger;
var collection = modules.collectionAccess.collection(collectionName);
var Moment = modules.moment;
var rc = modules.requestContext;
// This function is invoked when the client specifies custom resolution
var customEntityMerge = function(clientEntity, serverEntity){
var mergedEntity = {};
// Insert your custom logic here
mergedEntity = clientEntity;
return mergedEntity;
};
// This function is invoked when the client wins
var clientWins = function(){
response.continue();
};
// This function is used to report that a conflict was detected and report
// the conflict to the client
var reportConflict = function(debugInfo){
response.body = {
error: "EntityConflict",
description: "There was a conflict saving this entity, resolve using the custom property " + conflictPropertyName,
debug: debugInfo
};
// Set the HTTP status code for conflict
return response.complete(409);
};
// This function is invoked when the server wins
var serverWins = function(entity){
// Make the save look successful, just use the server entity
response.body = entity;
response.complete(201);
};
// If we have no incoming metadata or timestamp, client wins
if (!request.body._kmd) return clientWins();
if (!request.body._kmd.lmt) return clientWins();
var clientRootTime = Moment(request.body._kmd.lmt);
var id = request.body._id;
// Check for a server copy of the incoming entity
collection.findOne(id, function(err, serverEntity){
var clientEntity = req.body;
// If we hit an error bail out
if (err){
return response.error(err);
}
// No results means doesn't exist on server, client wins
if (serverEntity === null){
return clientWins();
}
var serverUpdateTime = Moment(serverEntity._kmd.lmt);
if (clientRootTime.isBefore(serverUpdateTime)){
// The server has been updated since the client received the entity
var conflictPolicy = rc.getCustomRequestProperty(conflictPropertyName);
switch (conflictPolicy){
case undefined:
case null:
// We don't have a policy set, let the client know
return reportConflict(JSON.stringify(serverEntity));
case "client":
return clientWins();
case "server":
return serverWins(serverEntity);
case "merged":
return clientWins();
case "server-logic":
case "custom-logic":
// The entity to save is the result of the merge
request.body = customEntityMerge(clientEntity, serverEntity);
return response.continue();
default:
return response.error(new Error("Invalid conflict policy."));
}
} else {
// The client is updating the latest server data
return clientWins();
}
});
}
Customization
There are several main points of customization for the provided sample code.
Server custom logic
The provided customEntityMerge
function just implements a client wins
policy. This can easily be overridden to implement complex merges that are
enforced by the server and are more secure than if the client implemented the
merge. To enforce the extra security see
Server enforced logic on how to prevent the client
and server
policies from being used. If the custom merge is being used for
efficiency only, then you can leave all cases enabled and let the client
choose the most efficient policy.
For example, let's say we're implementing an audio control system where we can adjust the volume and other properties about an audio speaker from multiple clients. We want to prevent damage and audience discomfort by preventing clients causing the volume to jump while still letting other settings be updated.
To accomplish this we can implement the following customEntityMerge
:
// This function is invoked when the client specifies custom resolution
var customEntityMerge = function(clientEntity, serverEntity){
// The merged entity looks like the client update
var mergedEntity = clientEntity;
// except for the volume, which takes the server version
mergedEntity.volume = serverEntity.volume;
return mergedEntity;
};
Server enforced logic
To allow the server to force a conflict resolution policy, you can disable the
non-desired policies by having their cases report an error (response.error
)
or just report the conflict (reportConflict
).
For example, to only allow custom server merges the case statement should be:
switch (conflictPolicy){
case "client":
case "server":
case "merged":
return reportConflict("Invalid conflict policy, policy must be server-logic or custom-logic.");
case undefined:
case null:
case "server-logic":
case "custom-logic":
// The entity to save is the result of the merge
request.body = customEntityMerge(clientEntity, serverEntity);
return response.continue();
default:
return response.error(new Error("Invalid conflict policy."));
}
Or you could disallow custom server logic with:
switch (conflictPolicy){
case undefined:
case null:
// We don't have a policy set, let the client know
return reportConflict(JSON.stringify(serverEntity));
case "client":
return clientWins();
case "server":
return serverWins(serverEntity);
case "merged":
return clientWins();
case "server-logic":
case "custom-logic":
return response.error(new Error("Conflict policy is not allowed."));
default:
return response.error(new Error("Invalid conflict policy."));
}
Large entities
The default implementation of the detection logic returns the server's entity
to the client in the debug
property of the conflict response. If entities
are large this can cause a performance issue. In that case the code should
do one of the following:
- Remove the returned entity and remove support for the
merged
policy. - Filter the large entity being returned to the client to a subset of large entity that the client can use to merge its entity. Then, implement a custom merge handler on the server.
- Use a Default conflict policy on the client to avoid reaching the merge condition.
Client-side usage
Each Kinvey client library provides a way to set custom request properties.
To use the conflict detection business logic you must set one of the supported
conflict resolution policies as the value of the ConflictResolutionWinner
custom property.
Policies
The valid policies (see Server enforced logic for reasons why some of the valid policies may be rejected) are:
Policy | Function Called | Meaning |
---|---|---|
client | clientWins() | Client should win the conflict |
server | serverWins(serverEntity) | Server should win the conflict |
merged | clientWins() | Client has returned an updated, merged, entity |
server-logic | customEntityMerge(clientEntity, serverEntity) | Server should merge the entities and use the result |
custom-logic | customEntityMerge(clientEntity, serverEntity) | Alias for server-logic |
Types of detection
Clients can either choose to set a default conflict policy, or they can choose to handle conflicts only when they are detected.
Default conflict policy
To set a default conflict policy, set the custom request property
ConflictResolutionWinner
on every request sent to the server. The server
should perform the requested action automatically on each request without the
client needing to know about the conflicts. In this mode the merged
policy
is not meaningful.
Detect and handle
In this mode the client should not set any value for the
ConflictResolutionWinner
. If a conflict is detected the server will respond
with an HTTP status code of 409 and contain the server's version of the entity
in the debug
property in JSON format. The client can either respond with
the correct resolution policy or merge the entities. If the client is going to
merge the entities, the client should deserialize the server JSON data and do a
merge between the server data and the data the client wants to save. Once the
entities have been merged the client can resubmit the entity with the
ConflictResolutionWinner
request property set to merged
.