Enhanced Conflict Resolution

    +

    Description — This content covers conflict resolution in inter-Sync Gateway replication
    Abstract — Inter-Sync Gateway replication conflict resolution policies and behaviors
    Related Content — Configuration Details | Admin REST API

    Context Clarification

    This content relates only to inter-Sync Gateway replication in Sync Gateway 2.8+. For documentation on pre-2.8 inter-Sync Gateway replication (also known as SG Replicate) — see Inter-Sync Gateway Replication pre-2.8

    Automatic Conflict Resolution

    Inter-Sync Gateway replication supports automatic conflict resolution to resolve conflicting document changes.

    It delivers this by applying one of its built-in conflict resolver policiesglossary icon, which can be easily included in your own replications.

    The goal of automatic conflict resoluton is to return a winning revision based on the consistent application of the configured conflict resolver policyglossary icon.

    The default conflict resolver policy is to always returns a winner determined by the automatic conflict resolution policyglossary icon.

    For ENTERPRISE EDITION, a Custom Conflict Resolver policy is available, providing additional flexibility by allowing users to provide their own conflict resolution logic.

    Conflict Response on Active Replicator

    As soon as the active Sync Gateway database detects a conflict in a replicated document revision, it initiates its configured conflict resolver policy to determine a winning revision. This policy assesses the conflicting revisions and either determines the winning revision or returns an error if it fails while doing so.

    Conflict Response by Passive Replicator

    When a passive Sync Gateway database detects a conflict it responds to the active with a 409 response and rejects the revision. It is expected that the active Sync Gateway will pull the conflicting revision from the passive, resolve it on the active, and then subsequently push the resolved conflict back up.

    How Resolution Works

    Pull Replications

    For Pull replications the active Sync Gateway is responsible for detecting and resolving conflicts based on the configured conflict_resolution_type  — see configuration item: conflict_resolution_type.

    This is also how conflicts are handled when Couchbase Lite clients pull down documents to Sync Gateway.

    Note: Resolved conflicts are only transferred from active to passive Sync Gateways if a replication is setup between them.

    Push Replications
    Passive Sync Gateway

    The passive Sync Gateway will automatically detect and reject conflicting revisions being pushed to it.

    Note that conflicts are not resolved. The revision is rejected and the document returned — with a 409 Conflict — response to the active Sync Gateway.

    Active Sync Gateway

    It is the responsibility of the active sync Gateway to address rejected revisions in accordance with its specified conflict_resolution_type.

    This approach is the same as that adopted when Couchbase Lite clients push documents to Sync Gateway.

    Configure Conflict Resolution

    Invoke automatic conflict resolution by specifying the required conflict resolver policy in the replication definitionglossary icon. The specified policy is applied whenever a conflict is detected.

    Example 1. Using automatic conflict resolution
    • default

    • localWins

    • remoteWins

    "databases:"
      // other config as necessary
      "this_db:"
        // other config as necessary
        "sgreplicate_enabled": "true",
        "replications": [
            {
              "replication_id": "replication1",
              "direction": "push_and_pull",
              "continuous": true,
              "filter": "sync_gateway/bychannel",
              "query_params": [
                  "channel1",
                  "channel2"
              ],
              "conflict_resolution_type": "default",
              // other config as necessary
            }
        ]
    // other config as necessary
    "databases:"
      // other config as necessary
      "this_db:"
        // other config as necessary
        "sgreplicate_enabled": "true",
        "replications": [
            {
              "replication_id": "replication1",
              "direction": "push_and_pull",
              "continuous": true,
              "filter": "sync_gateway/bychannel",
              "query_params": [
                  "channel1",
                  "channel2"
              ],
              "conflict_resolution_type": "localWins",
              // other config as necessary
            }
        ]
    // other config as necessary
    "databases:"
      // other config as necessary
      "this_db:"
        // other config as necessary
        "sgreplicate_enabled": "true",
        "replications": [
            {
              "replication_id": "replication1",
              "direction": "push_and_pull",
              "continuous": true,
              "filter": "sync_gateway/bychannel",
              "query_params": [
                  "channel1",
                  "channel2"
              ],
              "conflict_resolution_type": "remoteWins",
              // other config as necessary
            }
        ]
    // other config as necessary

    Build a Conflict Resolution Policy [EE]

    Overview

    Custom conflict resolution is handled by the active Sync Gateway using a user-provided custom conflict resolverglossary icon. This Javascript function is embedded in the replication configuration.

    The predefined conflict resolver policies are also available as Javascript functions that you can call from within that custom_conflict_resolver function This is useful when you want to apply greater selectivity to the automatic conflict resolution process. For example, you want to apply a 'remote wins' policy only for a specific type of document - see the 'Use Policies' tab in Example 4.

    Conflict Resolution Approaches

    There are two ways to handle conflicts in your custom_conflict_resolver, you can either:

    • Choose a winning revision from among the conflicting revisions (see Example 4), or

    • Merge conflicting revision to create a new winning revision; losing revisions are tomb-stoned.

      However, users should avoid overly-complex resolver logic that may impact performance.

    Approaches to Error Handling

    Your custom conflict resolver function should not terminate the replication when it encounters exceptions or errors. Instead, you should log sufficient information to aid troubleshooting and recovery.

    For example, your custom conflict resolver function should:

    • Skip the document causing the issue

    • Log a suitable warning level message. Include at least the skipped document’s Id and the sequence Id of the revision in error.

    Refer to log files when troubleshooting conflict resolution errors, to identify the document id and revision sequence in error.

    Example 2. Some Error Scenarios and Recommended Resolutions
    Unexpected data in the remote document

    You should update the remote document to fix the issue. Doing so will cause replication of the update.

    Unexpected data in the local document

    You should update the local document to fix the issue. This will not trigger a pull-replication. Do a no-op-update [1] of the remote document, which will trigger replication and conflict resolution.

    Fault in conflict resolution javascript function

    Fix the Javascript logic and then either:

    • Do a no op update [1] of the remote document. This triggers a pull replication and subsequent conflict resolution.

    • Reset the replication (using _replicationstatus/reset endpoint). Not recommended as it introduces significant duplicate processing in re-syncing previously synced documents.

    Conflict Resolver Structure

    This example shows the basic structure of the conflict resolver function as it would be defined in the configuration file.

    Example 3. Conflict resolver structure
    
    "custom_conflict_resolver": "`function(conflict) { (1)
      //  . . .
      //  . . . application logic to determine winner
      //  . . .
      return conflict.LocalDocument;  (2)
    }`" (3)
    1 The conflict structure comprises both conflicting documents.
    type Conflict struct {
    	LocalDocument  Body `json:"LocalDocument"`
    	RemoteDocument Body `json:"RemoteDocument"`
    }
    LocalDocument

    This LocalDocument object encapsulates the body and metadata of the local conflicting document revision being replicated. Its content matches the JSON stored at the local Sync Gateway.

    RemoteDocument

    The RemoteDocument object, encapsulates the body and metadata of the remote conflicting document revision being replicated. Its content matches the JSON stored at the remote Sync Gateway.

    2 You should return one of:
    • conflict.LocalDocument

    • conflict.RemoteDocument

    • a new document body comprising the merged local and remote documents

    • a nil body, which will be resolved as a delete

    3 The conflict resolver function is enclosed by backticks (``)

    Sample Conflict Resolvers

    Example 4. Simple conflict resolvers
    • Use Built-in Policies

    • Nominate a Winner

    • Merge a Winner

    This example uses the built-in resolver functions to resolve the conflict based-on the document type.

    So, documents of type a-doc-type-1 are always resolved in favor of the remote revision. All other document types are resolved in accordance with the default resolver policy.

    "replications": [
      {
      "replication_id": "replication1",
      // other config as required
      "conflict_resolution_type": "custom",
      "custom_conflict_resolver": `
        function(conflict) {
          if  (conflict.LocalDocument.type == "a-doctype-1") &&
              (conflict.RemoteDocument.type == "a-doctype-1")
           {
             // Invoke the built in default resolver logic
             return defaultPolicy(conflict);
           }
          else {
            // Otherwise resolve in favor of remote document
              return conflict.RemoteDocument;
            }
        }
        `
      // other config as required
      }
    ]

    This example selects a winner based on relative priorities and builds a return response of its own rather than using either the localWins or remoteWins policy, although it does rely on the default resolver policy as a backstop.

    "replications": [
      {
        // . . . preceding replication details as required
      },
      {
        "replication_id": "replication2",
        // . . .   other config as required
        "conflict_resolution_type": "custom",
        "custom_conflict_resolver": `
          function(conflict) {
            // Custom conflict resolution policy based on priority
            if (conflict.LocalDocument.body.priority > conflict.RemoteDocument.body.priority) {
              // Choose a local winner
              // Optionally apply application logic to manipulate
              // the local object before returning it as the winner
              return conflict.LocalDocument;
            } else if (local.body.priority < remote.body.priority) {
                // Choose a remote winner
                // Optionally apply application logic to manipulate
                // the remote object before returning it as the winner
              return conflict.RemoteDocument;
              }; //end if
            } //end func()
            // Apply the default policy as a catch all
            return defaultPolicy(conflict);
        }` // end resolver property
      }, // end replication2
      {
        // . . . further replication details as required
      }
    ]
    // . . .   other config as required

    This example creates a winner by merging changes from the local and remote documents to create a new document object, which is returned as the winner.

    If both document.types are non-null and the local document.type is usedefault, the merge path is overridden and the default resolver policy is applied.

    "custom_conflict_resolver":`
      function(conflict) {
          if (  (conflict.LocalDocument.type != null) &&
                (conflict.RemoteDocument.type != null) &&
                (conflict.LocalDocument.type == "usedefault"))
          {
              console.log("Will use default policy");
              // Resolve using built-in policy
              return defaultPolicy(conflict);
          }
          else
          {
            // Merge local and remote docs
            var remoteDoc = conflict.RemoteDocument;
            console.log("full remoteDoc doc: "+JSON.stringify(remoteDoc));
            var localDoc = conflict.LocalDocument;
            console.log("full localDoc doc: "+JSON.stringify(localDoc));
            var mergedDoc = extend({}, localDoc, remoteDoc);
    
            console.log("full mergedDoc doc: "+JSON.stringify(mergedDoc));
            // Resolve using this merged doc as the winner
            return mergedDoc;
    
            function extend(target) {
                var sources = [].slice.call(arguments, 1);
                sources.forEach(function (source) {
                    for (var prop in source) {
                        target[prop] = source[prop];
                    }
                });
                return target;
            } // end function extend()
          } // end if
      }` // end function()

    1. No-op update — refers to a change to the document body that has no impact on the app logic but will trigger an import by the Sync Gateway. One option could be to include a property used specifically for this purpose (i.e. a counter that can be incremented in response to conflict resolver errors).