Conflict Resolution on CBL Leaving large _deleted bodymap

Hi,

We’re having difficulties with our conflict resolution. We seem to be selecting a winner, but but each element in the bodyMap within the sync of the document contains a complete copy of the document-- Even though the elements are marked as _deleted.
Unfortunately, this is resulting in documents over the recommended size, and, consequently, we’re having more issues because of these large documents.

We have followed this guide: Couchbase Capella for Mobile Developers
to create our CBL conflict resolution.
For Example, the document will look something like this after resolving a conflict:

{
“_id”: “-…”,
“_rev”: “12-…”,
“_sync”: {
“rev”: “12-…”,
“new_rev”: “18-…”,
“flags”: 20,
“sequence”: 1998,
“recent_sequences”: [
37,
40,
82,
83,
155,
156,
964,
1763,
1765,
1989,
1997,
1998
],
“history”: {
“revs”: [
“15-…”,
“7-…”,
“16-…”,
“8-…”,
“14-…”,
“10-…”,
“3-…”,
“9-…”,
“17-…”,
“2-…”,
“11-…”,
“11-…”,
“8-…”,
“18-…”,
“10-…”,
“6-…”,
“7-…”,
“12-…”,
“13-…”,
“4-…”,
“1-…”,
“12-…”,
“9-…”,
“5-…”
],
“parents”: [
4,
15,
0,
16,
18,
22,
9,
12,
2,
20,
14,
5,
1,
8,
7,
23,
15,
10,
17,
6,
-1,
11,
3,
19
],
“deleted”: [
13
],
“bodymap”: {
“13”: “{"_deleted":true,"createdAt":"","versionNum":""}”
},
“channels”: [
null,
[“test”]
]
},
“channels”: {
“test”: null
},
“time_saved”: “2017-07-10T18:47:10.770180927Z”
},
“createdAt”: “2017-07-10T18:39:14.961Z”,
“versionNum”: “V1.1”
}

Should the bodymap contain complete copies of the document-- even after the conflict has been resolved?
Is there any way to get around saving these large sync.history.bodymaps?
We are using CBL 1.4.0.1

Any suggestions will be appreciated,
Thanks

This blog may help you understand the pruning algorithm and the impact of conflict resolution on the same. Pruning removes bodies and metadata associated with non-leaf nodes. The tombstoned revision only contain basic info (_deleted flag) that indicates that the revision is deleted and should not contain entire message body.

Hi @priya.rajagopal, thanks for the quick reply.

Thanks for clarifying the pruning algorithm.,

However, for our case, we’d like a way to minimize the size of the sync until pruning/compaction takes place.

Using the provided CBL Conflict resolution test project, we came up with a few test scenarios:

Method 1 (Leaving entire documents in the bodymap):

func resolveConflicts(revisions revs: [CBLRevision], withProps desiredProps: [String: Any]?, andImage desiredImage: CBLAttachment?) {
 database.inTransaction {
        var i = 0
        for rev in revs as! [CBLSavedRevision] {
            let newRev = rev.createRevision()  // Create new revision
            if (i == 0) { // That's the current / winning revision
                
                newRev.userProperties = desiredProps // Set properties to desired properties
                if rev.attachmentNamed("image") != desiredImage {
                    newRev.setAttachmentNamed("image", withContentType: "image/jpg", content: desiredImage?.content)
                }
            } else {
                // That's a conflicting revision, delete it
                newRev.isDeletion = true
            }
            do {
                try newRev.saveAllowingConflict()  // Persist the new revisions
            } catch let error as NSError {
                NSLog("Cannot resolve conflicts with error: %@", error)
                return false
            }
            i += 1
        }
        return true
    }
}

And a document with resolved conflicts looks like this:

[details=doc1]> {

“_sync”:{
“rev”:“3-46bb09aa101b84e555c0b7aee743a5b6”,
“new_rev”:“3-4b8183fb6da995ba83ffed7c46fdbd1e”,
“flags”:20,
“sequence”:158,
“recent_sequences”:[
148,
149,
150,
151,
152,
153,
154,
155,
156,
157,
158
],
“history”:{
“revs”:[
“2-01558dcee47d4d04e617f4d1af0e92b8”,
“3-3532482a66de5f3dcbeeb6cab4496d1e”,
“3-4b8183fb6da995ba83ffed7c46fdbd1e”,
“1-d2e116c4a32ba7a1c6f5d4e81a920368”,
“3-2f26db7ea4ea9fbb3da3ea934584f147”,
“2-a3881738195c82f2e7cd650dcd4eb73a”,
“2-04e44928a189119a4c62eeee1daa8376”,
“2-70eebc24ba94129afa2ac1577fe33401”,
“3-46bb09aa101b84e555c0b7aee743a5b6”,
“2-fed5f0c3f516e077a2d545a96c03bf16”,
“3-fe88f6a6c64407eb404e5f0eb97ecc92”
],
“parents”:[
3,
6,
0,
-1,
7,
3,
3,
3,
9,
3,
5
],
“deleted”:[
1,
2,
4,
10
],
“bodymap”:{
“1”:“{"_deleted":true,"name":"A Fun Game","owner":"todo","type":"task-list"}”,
“10”:“{"_deleted":true,"name":"Table Soccer","owner":"todo","type":"task-list"}”,
“2”:“{"_deleted":true,"name":"Foosball","owner":"todo","type":"task-list"}”,
“4”:“{"_deleted":true,"name":"Fuzboll","owner":"todo","type":"task-list"}”
},
“channels”:[
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null
]
},
“time_saved”:“2017-07-11T14:08:48.291480628-05:00”
},
“name”:“Table Football”,
“owner”:“todo”,
“type”:“task-list”
}[/details]

As you can see, complete documents are stored in the body map.

After some work, found a way to only store > {"_deleted":true}
In the body map–per resolved document

Here’s our method:

Method 2 (complete documents are not stored in the bodymap):

func resolveConflicts(revisions revs: [CBLRevision], withProps desiredProps: [String: Any]?,  andImage desiredImage: CBLAttachment?) {
        database.inTransaction {
        for rev in revs as! [CBLSavedRevision] {
            
            print(rev.properties)
            if (rev.properties?["name"]! as? String == "Foosball") { // That's the current / winning revision
                let newRev = rev.createRevision()  // Create new revision
                
                if rev.attachmentNamed("image") != desiredImage {
                    newRev.setAttachmentNamed("image", withContentType: "image/jpg", content: desiredImage?.content)
                }
                do {
                    try newRev.saveAllowingConflict()  // Persist the new revisions
                } catch let error as NSError {
                    NSLog("Cannot resolve conflicts with error: %@", error)
                    return false
                }
                
            } else { // That's a conflicting revision, delete it
                do{
                    try rev.deleteDocument()
                } catch let error as NSError{
                    NSLog("Cannot resolve conflicts with error: %@", error)
                    return false
                }
            }
        }
        return true
    }
}

And the document looks like this:

[details=doc2]> {

“_sync”:{
“rev”:“3-a5b50c242841a8f94ee9e867f44aac57”,
“flags”:16,
“sequence”:146,
“recent_sequences”:[
136,
137,
138,
139,
140,
141,
142,
143,
144,
145,
146
],
“history”:{
“revs”:[
“3-a5b50c242841a8f94ee9e867f44aac57”,
“3-05fd6280bb2b5e667655d9046ce55970”,
“1-d2e116c4a32ba7a1c6f5d4e81a920368”,
“2-a3881738195c82f2e7cd650dcd4eb73a”,
“3-26138aad7f1aeb7c095612cb5bb53b18”,
“2-70eebc24ba94129afa2ac1577fe33401”,
“2-01558dcee47d4d04e617f4d1af0e92b8”,
“3-1470621fa2bb08e2908e7f161b6e71cd”,
“2-fed5f0c3f516e077a2d545a96c03bf16”,
“3-efbe53deec36eef18a532054bdaabccf”,
“2-04e44928a189119a4c62eeee1daa8376”
],
“parents”:[
6,
8,
-1,
2,
5,
2,
2,
3,
2,
10,
2
],
“deleted”:[
1,
4,
7,
9
],
“bodymap”:{
“1”:“{"_deleted":true}”,
“4”:“{"_deleted":true}”,
“7”:“{"_deleted":true}”,
“9”:“{"_deleted":true}”
},
“channels”:[
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null
]
},
“time_saved”:“2017-07-11T13:58:53.553483182-05:00”
},
“name”:“Foosball”,
“owner”:“todo”,
“type”:“task-list”
}[/details]

Note, the body map only contains “…”:“{"_deleted":true}” elements

The second method seems like it may work for our use. However, I’m concerned with using rev.deleteDocument() instead of simply setting isDeletion to true. Also, in our second method of conflict resolution, we only use createRevision() on the winning conflict.

What is the expected behavior, and should we be concerned that we are only storing “:”{"_deleted":true} in the elements of the bodymap? I’m not quite sure why, after resolving conflicts, we would need access to entire documents in the bodymap.

Thanks Again,
Caroline

Setting the isDeletion property of a revision just adds the _deleted property; it doesn’t remove any of the other document properties. You could delete the other properties from the dictionary, or just call deleteDocument as you’re doing.

Thanks for clearing that up @jens