bl-conflict-resolution

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

  1. Created a Kinvey app.
  2. Are familiar with a Kinvey client library.
  3. 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:

PolicyFunction CalledMeaning
clientclientWins()Client should win the conflict
serverserverWins(serverEntity)Server should win the conflict
mergedclientWins()Client has returned an updated, merged, entity
server-logiccustomEntityMerge(clientEntity, serverEntity)Server should merge the entities and use the result
custom-logiccustomEntityMerge(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.

Got a question?