Resolving Conflicts with iOS CBL

178 views
Skip to first unread message

Scott Ahten

unread,
Oct 28, 2014, 2:41:11 PM10/28/14
to mobile-c...@googlegroups.com
I'm having difficulty resolving conflicts on a document with a large number of existing conflicts (100+) on iOS. 

Currently, my conflict resolution method is based on the Objective-C example here:http://developer.couchbase.com/mobile/develop/guides/couchbase-lite/native-api/document/index.html#Understanding%20Conflicts 

I've simplified our resolution method to simply use the "winning" properties set for the current document. 

- (void)resolveConflictsForDocument:(CBLDocument*)doc
{
   
NSError *error;
   
NSArray *conflicts = [doc getConflictingRevisions: &error];
   
if (conflicts.count > 1) {
       
       
NSLog(@%lu total Conflicts for: %@", (unsigned long)conflicts.count, doc.documentID);
       
        [self.database inTransaction:^BOOL{
       
            // For now, pick the current properties for the document
            NSMutableDictionary* mergedProps =[doc.properties mutableCopy];
           
            // Delete the conflicting revisions to get rid of the conflict:
            CBLSavedRevision* current = doc.currentRevision;
         
            int revisionNumber = 0;
            for (CBLSavedRevision* rev in conflicts) {
                BOOL wasCurrent = NO;                
                CBLUnsavedRevision *newRev = [current createRevision];
                if (rev == current) {
                    newRev.properties = mergedProps; // add the merged revision
                    wasCurrent = YES;
                } else {
                    newRev.isDeletion = YES;  // mark other conflicts as deleted
                }
                NSError *error;
                [newRev save: &error];
               
                NSLog(@"
Saved %@ revsion number %d of documment: %@ error:%@", wasCurrent ? @"current" : @"non-current", revisionNumber, doc.documentID, error.localizedDescription );
                revisionNumber++;
            }
           
            return YES;
        }];
    }
}

This seems to work correctly on documents with a small number of conflicts, but saves return 409 conflict errors on two specific documents with six and 107 conflicts. Here's an example output from the six conflict document. 

6 total Conflicts for: f8ee7974-9558-4ba4-ba94-6a7ce98c58b4
Saved current revsion number 0 of documment: f8ee7974-9558-4ba4-ba94-6a7ce98c58b4 error:(null)
Saved non-current revsion number 1 of documment: f8ee7974-9558-4ba4-ba94-6a7ce98c58b4 error:409 conflict
Saved non-current revsion number 2 of documment: f8ee7974-9558-4ba4-ba94-6a7ce98c58b4 error:409 conflict
Saved non-current revsion number 3 of documment: f8ee7974-9558-4ba4-ba94-6a7ce98c58b4 error:409 conflict
Saved non-current revsion number 4 of documment: f8ee7974-9558-4ba4-ba94-6a7ce98c58b4 error:409 conflict
Saved non-current revsion number 5 of documment: f8ee7974-9558-4ba4-ba94-6a7ce98c58b4 error:409 conflict

"non-current" revisions are tombstoned.

I've also included a run with logging enabled for sync and database enabled for both normal and verbose variants which also includes output from compacting the entire database. The output can be found here: https://gist.github.com/lightandshadow68/c7dd2ebacef2d00adff9

Is there something special we need to do with documents that have more that two conflict revisions? 

Also, does getConflictingRevisions: return just the conflicting revisions or all revisions in the tree? That is, when attempting to resolve multiple conflicts, are some revisions returned here part of the tree that leads to the current revision and therefore cannot be tombstoned?

Thanks, 

- Scott

Jens Alfke

unread,
Oct 28, 2014, 2:57:26 PM10/28/14
to mobile-c...@googlegroups.com
You need to save the new revisions with -saveAllowingConflict:, otherwise they'll be rejected because their parent isn't the document's current revision.

(I'll go fix our example code if it has this mistake.)

> Also, does getConflictingRevisions: return just the conflicting revisions or all revisions in the tree?

It returns all the "leaf" revisions. So no ancestors.

—Jens

Scott Ahten

unread,
Oct 28, 2014, 5:03:37 PM10/28/14
to mobile-c...@googlegroups.com

On Tuesday, October 28, 2014 2:57:26 PM UTC-4, Jens Alfke wrote:
You need to save the new revisions with -saveAllowingConflict:, otherwise they'll be rejected because their parent isn't the document's current revision. 
 
Using -saveAllowingConflict: prevents a 409 error, but when I run the process again the conflicts remain. Do I need to perform any other steps other than compaction on the client?
 

Jens Alfke

unread,
Oct 28, 2014, 11:57:49 PM10/28/14
to mobile-c...@googlegroups.com

On Oct 28, 2014, at 2:03 PM, Scott Ahten <lightand...@gmail.com> wrote:

Using -saveAllowingConflict: prevents a 409 error, but when I run the process again the conflicts remain.

How are you seeing that the conflicts remain? Does -conflictingRevisions still return them after you finish resolving the conflict? (If so, that's very strange…)

—Jens

Scott Ahten

unread,
Oct 29, 2014, 11:58:38 AM10/29/14
to mobile-c...@googlegroups.com
On Tuesday, October 28, 2014 11:57:49 PM UTC-4, Jens Alfke wrote:

How are you seeing that the conflicts remain? Does -conflictingRevisions still return them after you finish resolving the conflict? (If so, that's very strange…)

Yes. Subsequent calls to -getConflictingRevisions: returns the same number of conflicts. (Strange indeed...)

Here's the output from two runs....  

10 total Conflicts for: f8ee7974-9558-4ba4-ba94-6a7ce98c58b4
Saved current for revsion 119-36bd4f3d615121925ea10e464dff9e84 of documment: f8ee7974-9558-4ba4-ba94-6a7ce98c58b4 error:(null)
Saved non-current for revsion 116-b9df055884bcd38a28d4db14582c608a of documment: f8ee7974-9558-4ba4-ba94-6a7ce98c58b4 error:(null)
Saved non-current for revsion 115-4643765c558bddde1eddb0ebd71039c5 of documment: f8ee7974-9558-4ba4-ba94-6a7ce98c58b4 error:(null)
Saved non-current for revsion 113-3eb619c463767d3449652dc553c196a9 of documment: f8ee7974-9558-4ba4-ba94-6a7ce98c58b4 error:(null)
Saved non-current for revsion 114-b119801ccce39c63560dd7a3576a38ea of documment: f8ee7974-9558-4ba4-ba94-6a7ce98c58b4 error:(null)
Saved non-current for revsion 112-edd492acdcaef62e8fbe83f8c7b7fedd of documment: f8ee7974-9558-4ba4-ba94-6a7ce98c58b4 error:(null)
Saved non-current for revsion 107-0aec4a3c4f6b41af463d052776f70c23 of documment: f8ee7974-9558-4ba4-ba94-6a7ce98c58b4 error:(null)
Saved non-current for revsion 106-06ab0852a09ffd7a74a8679ae9e6d2c6 of documment: f8ee7974-9558-4ba4-ba94-6a7ce98c58b4 error:(null)
Saved non-current for revsion 111-0ae2df3e9cafc2bb407323ab57e6c2c9 of documment: f8ee7974-9558-4ba4-ba94-6a7ce98c58b4 error:(null)
Saved non-current for revsion 110-5502833a249b6ce4f7ca6e2ea2b40cb7 of documment: f8ee7974-9558-4ba4-ba94-6a7ce98c58b4 error:(null)


10 total Conflicts for: f8ee7974-9558-4ba4-ba94-6a7ce98c58b4
Saved current for revsion 120-c438665ca9877ee388d91fbb71f3d33f of documment: f8ee7974-9558-4ba4-ba94-6a7ce98c58b4 error:(null)
Saved non-current for revsion 116-b9df055884bcd38a28d4db14582c608a of documment: f8ee7974-9558-4ba4-ba94-6a7ce98c58b4 error:(null)
Saved non-current for revsion 115-4643765c558bddde1eddb0ebd71039c5 of documment: f8ee7974-9558-4ba4-ba94-6a7ce98c58b4 error:(null)
Saved non-current for revsion 113-3eb619c463767d3449652dc553c196a9 of documment: f8ee7974-9558-4ba4-ba94-6a7ce98c58b4 error:(null)
Saved non-current for revsion 114-b119801ccce39c63560dd7a3576a38ea of documment: f8ee7974-9558-4ba4-ba94-6a7ce98c58b4 error:(null)
Saved non-current for revsion 112-edd492acdcaef62e8fbe83f8c7b7fedd of documment: f8ee7974-9558-4ba4-ba94-6a7ce98c58b4 error:(null)
Saved non-current for revsion 107-0aec4a3c4f6b41af463d052776f70c23 of documment: f8ee7974-9558-4ba4-ba94-6a7ce98c58b4 error:(null)
Saved non-current for revsion 106-06ab0852a09ffd7a74a8679ae9e6d2c6 of documment: f8ee7974-9558-4ba4-ba94-6a7ce98c58b4 error:(null)
Saved non-current for revsion 111-0ae2df3e9cafc2bb407323ab57e6c2c9 of documment: f8ee7974-9558-4ba4-ba94-6a7ce98c58b4 error:(null)
Saved non-current for revsion 110-5502833a249b6ce4f7ca6e2ea2b40cb7 of documment: f8ee7974-9558-4ba4-ba94-6a7ce98c58b4 error:(null)

Looking at the code, it's always calling createRevision on the revision we want to keep, then saving it's new revision, even if the revision from the enumeration is not the current revision. Should it be creating a new revision with the actual conflicting revision returned in the enumeration, then saving that instead? Something like this...?

- (void)resolveConflictsForDocument:(CBLDocument*)doc
{
   
NSError *error;
   
NSArray *conflicts = [doc getConflictingRevisions: &error];
   
if (conflicts.count > 1) {

       
       
NSLog(@"%lu total Conflicts for: %@", (unsigned long)conflicts.count, doc.documentID);

       
       
[self.database inTransaction:^BOOL{
       
           
// For now, pick the current properties for the document
           
NSMutableDictionary* mergedProps =[doc.properties mutableCopy];
           
           
// Delete the conflicting revisions to get rid of the conflict:

           
CBLSavedRevision* currentRevision = doc.currentRevision;
         
           
int revisionNumber = 0;
           
for (CBLSavedRevision* conflictingRevision in conflicts) {
                BOOL wasCurrent
= NO;
               
               
CBLUnsavedRevision *newRevision = [conflictingRevision createRevision]; // <-- conflicting not current?
               
if (conflictingRevision == currentRevision) {
                    newRevision
.properties = mergedProps; // add the merged revision
                    wasCurrent
= YES;
               
} else {
                    newRevision
.isDeletion = YES;  // mark other conflicts as deleted
               
}
               
NSError *saveError;
               
[newRevision saveAllowingConflict: &saveError];
               
               
NSLog(@"Saved %@ for revsion %@ of documment: %@ error:%@", wasCurrent ? @"current" : @"non-current", newRevision.revisionID, doc.documentID, saveError.localizedDescription );
                revisionNumber
++;
           
}
           
           
return YES;
       
}];
   
}
}

Jens Alfke

unread,
Oct 29, 2014, 1:40:04 PM10/29/14
to mobile-c...@googlegroups.com

On Oct 29, 2014, at 8:58 AM, Scott Ahten <lightand...@gmail.com> wrote:

Looking at the code, it's always calling createRevision on the revision we want to keep, then saving it's new revision, even if the revision from the enumeration is not the current revision. Should it be creating a new revision with the actual conflicting revision returned in the enumeration, then saving that instead?

Aw crap, you're right — it's a mistake in my sample code. The line
                CBLUnsavedRevision *newRev = [current createRevision];
should be
                CBLUnsavedRevision *newRev = [rev createRevision];
which I think is the same change you made in the code you just posted.

I'll go fix the docs again. Thank you so much for noticing & debugging this.

—Jens

Scott Ahten

unread,
Oct 29, 2014, 3:04:30 PM10/29/14
to mobile-c...@googlegroups.com


On Wednesday, October 29, 2014 1:40:04 PM UTC-4, Jens Alfke wrote:

Aw crap, you're right — it's a mistake in my sample code. The line
                CBLUnsavedRevision *newRev = [current createRevision];
should be
                CBLUnsavedRevision *newRev = [rev createRevision];
which I think is the same change you made in the code you just posted.


Yes, that is effectively the same change. However, the conflicts still remain. 

Here's the output from one of the runs. What seems odd is that newly saved revisions do not have revision IDs, even after being saved. 

13 total Conflicts for: f8ee7974-9558-4ba4-ba94-6a7ce98c58b4
Saved current of new revision (null) for revsion 121-0c51102053b54432ea48dad131c5cf86 of documment: f8ee7974-9558-4ba4-ba94-6a7ce98c58b4 error:(null)
Saved non-current of new revision (null) for revsion 120-c438665ca9877ee388d91fbb71f3d33f of documment: f8ee7974-9558-4ba4-ba94-6a7ce98c58b4 error:(null)
Saved non-current of new revision (null) for revsion 119-36bd4f3d615121925ea10e464dff9e84 of documment: f8ee7974-9558-4ba4-ba94-6a7ce98c58b4 error:(null)
Saved non-current of new revision (null) for revsion 116-b9df055884bcd38a28d4db14582c608a of documment: f8ee7974-9558-4ba4-ba94-6a7ce98c58b4 error:(null)
Saved non-current of new revision (null) for revsion 106-06ab0852a09ffd7a74a8679ae9e6d2c6 of documment: f8ee7974-9558-4ba4-ba94-6a7ce98c58b4 error:(null)
Saved non-current of new revision (null) for revsion 110-5502833a249b6ce4f7ca6e2ea2b40cb7 of documment: f8ee7974-9558-4ba4-ba94-6a7ce98c58b4 error:(null)
Saved non-current of new revision (null) for revsion 107-0aec4a3c4f6b41af463d052776f70c23 of documment: f8ee7974-9558-4ba4-ba94-6a7ce98c58b4 error:(null)
Saved non-current of new revision (null) for revsion 118-a8bee8abf22403a04703d29dc8e84325 of documment: f8ee7974-9558-4ba4-ba94-6a7ce98c58b4 error:(null)
Saved non-current of new revision (null) for revsion 115-4643765c558bddde1eddb0ebd71039c5 of documment: f8ee7974-9558-4ba4-ba94-6a7ce98c58b4 error:(null)
Saved non-current of new revision (null) for revsion 113-3eb619c463767d3449652dc553c196a9 of documment: f8ee7974-9558-4ba4-ba94-6a7ce98c58b4 error:(null)
Saved non-current of new revision (null) for revsion 114-b119801ccce39c63560dd7a3576a38ea of documment: f8ee7974-9558-4ba4-ba94-6a7ce98c58b4 error:(null)
Saved non-current of new revision (null) for revsion 112-edd492acdcaef62e8fbe83f8c7b7fedd of documment: f8ee7974-9558-4ba4-ba94-6a7ce98c58b4 error:(null)
Saved non-current of new revision (null) for revsion 111-0ae2df3e9cafc2bb407323ab57e6c2c9 of documment: f8ee7974-9558-4ba4-ba94-6a7ce98c58b4 error:(null)
Staring Compact
Finished Compact

Our of curiosity, I opened the corresponding SQLite file and found 52 revisions for f8ee7974-9558-4ba4-ba94-6a7ce98c58b4. Running the process multiple times does not increase the number of revisions for this document. 

Furthermore, all of the revisions returning as a conflict above have a parent of NULL in the underlying SQLite database. They also have a "1" in their current column. Nor can I find a corresponding revision with the sequence number for one of these above revisions as its parent, which I would expect the conflict resolution code to create. 

select * from revs where doc_id = 5574 and current = 1 and parent is null and deleted is not 1 returns all of the revisions marked as a conflict. 

sequence|doc_id|revid|parent|current|deleted|no_attachments
26015|5574|"111-0ae2df3e9cafc2bb407323ab57e6c2c9"|<NULL>|1|0|1
35199|5574|"112-edd492acdcaef62e8fbe83f8c7b7fedd"|<NULL>|1|0|1
35337|5574|"114-b119801ccce39c63560dd7a3576a38ea"|<NULL>|1|0|1
35338|5574|"113-3eb619c463767d3449652dc553c196a9"|<NULL>|1|0|1
35358|5574|"115-4643765c558bddde1eddb0ebd71039c5"|<NULL>|1|0|1
36748|5574|"118-a8bee8abf22403a04703d29dc8e84325"|<NULL>|1|0|1
36855|5574|"107-0aec4a3c4f6b41af463d052776f70c23"|<NULL>|1|0|1
36859|5574|"110-5502833a249b6ce4f7ca6e2ea2b40cb7"|<NULL>|1|0|1
36860|5574|"106-06ab0852a09ffd7a74a8679ae9e6d2c6"|<NULL>|1|0|1
36861|5574|"116-b9df055884bcd38a28d4db14582c608a"|<NULL>|1|0|1
36870|5574|"119-36bd4f3d615121925ea10e464dff9e84"|<NULL>|1|0|1
36873|5574|"120-c438665ca9877ee388d91fbb71f3d33f"|<NULL>|1|0|1
36879|5574|"121-0c51102053b54432ea48dad131c5cf86"|<NULL>|1|0|1

So we seem to be looking at multiple roots in the revision tree. Is it possible the tree is somehow corrupt for this?

Scott Ahten

unread,
Oct 29, 2014, 3:15:43 PM10/29/14
to mobile-c...@googlegroups.com

On Wednesday, October 29, 2014 1:40:04 PM UTC-4, Jens Alfke wrote:

Aw crap, you're right — it's a mistake in my sample code. The line
                CBLUnsavedRevision *newRev = [current createRevision];
should be
                CBLUnsavedRevision *newRev = [rev createRevision];
which I think is the same change you made in the code you just posted.

I'll go fix the docs again. Thank you so much for noticing & debugging this.

—Jens

Posting again. I think Google Groups didn't like including SQL in my comment. 

- - - - - - - - -

Yes, they are effectively the same. However, the conflicts still remain. 

Here's the output from my latest run. What's odd is that newly created revisions do not have revision IDs even after being saved. 

13 total Conflicts for: f8ee7974-9558-4ba4-ba94-6a7ce98c58b4
Saved current of new revision (null) for revsion 121-0c51102053b54432ea48dad131c5cf86 of documment: f8ee7974-9558-4ba4-ba94-6a7ce98c58b4 error:(null)
Saved non-current of new revision (null) for revsion 120-c438665ca9877ee388d91fbb71f3d33f of documment: f8ee7974-9558-4ba4-ba94-6a7ce98c58b4 error:(null)
Saved non-current of new revision (null) for revsion 119-36bd4f3d615121925ea10e464dff9e84 of documment: f8ee7974-9558-4ba4-ba94-6a7ce98c58b4 error:(null)
Saved non-current of new revision (null) for revsion 116-b9df055884bcd38a28d4db14582c608a of documment: f8ee7974-9558-4ba4-ba94-6a7ce98c58b4 error:(null)
Saved non-current of new revision (null) for revsion 106-06ab0852a09ffd7a74a8679ae9e6d2c6 of documment: f8ee7974-9558-4ba4-ba94-6a7ce98c58b4 error:(null)
Saved non-current of new revision (null) for revsion 110-5502833a249b6ce4f7ca6e2ea2b40cb7 of documment: f8ee7974-9558-4ba4-ba94-6a7ce98c58b4 error:(null)
Saved non-current of new revision (null) for revsion 107-0aec4a3c4f6b41af463d052776f70c23 of documment: f8ee7974-9558-4ba4-ba94-6a7ce98c58b4 error:(null)
Saved non-current of new revision (null) for revsion 118-a8bee8abf22403a04703d29dc8e84325 of documment: f8ee7974-9558-4ba4-ba94-6a7ce98c58b4 error:(null)
Saved non-current of new revision (null) for revsion 115-4643765c558bddde1eddb0ebd71039c5 of documment: f8ee7974-9558-4ba4-ba94-6a7ce98c58b4 error:(null)
Saved non-current of new revision (null) for revsion 113-3eb619c463767d3449652dc553c196a9 of documment: f8ee7974-9558-4ba4-ba94-6a7ce98c58b4 error:(null)
Saved non-current of new revision (null) for revsion 114-b119801ccce39c63560dd7a3576a38ea of documment: f8ee7974-9558-4ba4-ba94-6a7ce98c58b4 error:(null)
Saved non-current of new revision (null) for revsion 112-edd492acdcaef62e8fbe83f8c7b7fedd of documment: f8ee7974-9558-4ba4-ba94-6a7ce98c58b4 error:(null)
Saved non-current of new revision (null) for revsion 111-0ae2df3e9cafc2bb407323ab57e6c2c9 of documment: f8ee7974-9558-4ba4-ba94-6a7ce98c58b4 error:(null)
Staring Compact
Finished Compact

Out of curiosity, I opened the corresponding SQLite file. There were 52 revisions for this document. Running the resolution process multiple times did not increase this count. Also, all of the revisions returned above have a parent of NULL and their current flag set to "1". Nor could I find any revisions with parents that match any of the above sequence numbers, which I would expect this process to create. 

When I queried for revisions for this document with a null parent and their current flag set to 1 I received the exact same group of revisions indicated as in conflict listed above. 

sequence|doc_id|revid|parent|current|deleted|no_attachments
26015|5574|"111-0ae2df3e9cafc2bb407323ab57e6c2c9"|<NULL>|1|0|1
35199|5574|"112-edd492acdcaef62e8fbe83f8c7b7fedd"|<NULL>|1|0|1
35337|5574|"114-b119801ccce39c63560dd7a3576a38ea"|<NULL>|1|0|1
35338|5574|"113-3eb619c463767d3449652dc553c196a9"|<NULL>|1|0|1
35358|5574|"115-4643765c558bddde1eddb0ebd71039c5"|<NULL>|1|0|1
36748|5574|"118-a8bee8abf22403a04703d29dc8e84325"|<NULL>|1|0|1
36855|5574|"107-0aec4a3c4f6b41af463d052776f70c23"|<NULL>|1|0|1
36859|5574|"110-5502833a249b6ce4f7ca6e2ea2b40cb7"|<NULL>|1|0|1
36860|5574|"106-06ab0852a09ffd7a74a8679ae9e6d2c6"|<NULL>|1|0|1
36861|5574|"116-b9df055884bcd38a28d4db14582c608a"|<NULL>|1|0|1
36870|5574|"119-36bd4f3d615121925ea10e464dff9e84"|<NULL>|1|0|1
36873|5574|"120-c438665ca9877ee388d91fbb71f3d33f"|<NULL>|1|0|1
36879|5574|"121-0c51102053b54432ea48dad131c5cf86"|<NULL>|1|0|1

So, it seems that we have a revision tree with multiple roots. Could it be that the revision tree for this document is corrupt? 

Scott Ahten

unread,
Oct 29, 2014, 5:12:01 PM10/29/14
to mobile-c...@googlegroups.com
Further investigation indicates all of the revisions being created are duplicates. 

For example.... 

16:55:47.152| CBLDatabase: Duplicate rev insertion: f8ee7974-9558-4ba4-ba94-6a7ce98c58b4 / 111-de503a5a2c1cacbe39e645466980888e

However, this revision does not have a parent. I'm guessing this is why 

sequence|doc_id|revid|parent|current|deleted|no_attachments
26886|5833|"111-de503a5a2c1cacbe39e645466980888e"|<NULL>|1|1|1

Should I create a GitHub issue and move this conversation there?


Scott Ahten

unread,
Oct 29, 2014, 5:33:24 PM10/29/14
to mobile-c...@googlegroups.com

On Wednesday, October 29, 2014 5:12:01 PM UTC-4, Scott Ahten wrote:

However, this revision does not have a parent. I'm guessing this is why [It's being flagged as a conflict]

After stepping though the code, I've noticed the duplicate inserts have the correct parent sequence number, but are being rejected due to a constraints failure. 

Jens Alfke

unread,
Oct 29, 2014, 6:42:30 PM10/29/14
to mobile-c...@googlegroups.com

On Oct 29, 2014, at 12:15 PM, Scott Ahten <lightand...@gmail.com> wrote:

So, it seems that we have a revision tree with multiple roots. Could it be that the revision tree for this document is corrupt? 

I think the revision tree has been pruned to limit it to the maxRevTreeDepth. The parents of these revisions got purged because they're too deep in the tree, but those revisions didn't because they're undeleted leaves (open conflicts.)

So the situation the database starts in is probably valid, although an edge case; but something's going wrong with creating the revisions that would delete the conflicts. 

Yes, please file an issue in Github. And if it's possible for you to send me the actual database file, that would be even better.

—Jens

Jens Alfke

unread,
Oct 30, 2014, 1:41:03 PM10/30/14
to mobile-c...@googlegroups.com
FYI, this is now being tracked as issue #509. It's definitely a CBL bug, but I haven't tracked it down yet.

—Jens
Reply all
Reply to author
Forward
0 new messages