Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions db/hybrid_logical_vector.go
Original file line number Diff line number Diff line change
Expand Up @@ -952,6 +952,15 @@ func DefaultLWWConflictResolutionType(ctx context.Context, conflict Conflict) (B
if conflict.LocalHLV == nil || conflict.RemoteHLV == nil {
return nil, errors.New("local or incoming document is nil for resolveConflict")
}
localDeleted := conflict.LocalDocument.IsDeleted()
remoteDeleted := conflict.RemoteDocument.IsDeleted()
if localDeleted && !remoteDeleted {
return conflict.LocalDocument, nil
}
if remoteDeleted && !localDeleted {
return conflict.RemoteDocument, nil
}

// resolve conflict in favor of remote document, remote wins case
if conflict.RemoteHLV.Version > conflict.LocalHLV.Version {
// remote document wins
Expand Down
6 changes: 6 additions & 0 deletions db/revision.go
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,12 @@ func (body Body) ExtractExpiry() (uint32, error) {
return exp, nil
}

// IsDeleted returns true if the body contains a _deleted property set to true.
func (body Body) IsDeleted() bool {
deleted, _ := body[BodyDeleted].(bool)
return deleted
}

func (body Body) ExtractDeleted() bool {
deleted, _ := body[BodyDeleted].(bool)
delete(body, BodyDeleted)
Expand Down
101 changes: 100 additions & 1 deletion rest/replicatortest/replicator_conflict_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1480,7 +1480,7 @@ func TestActiveReplicatorInvalidCustomResolver(t *testing.T) {
_, _, _, err = rt2Collection.PutExistingCurrentVersion(rt2Ctx, opts)
require.NoError(t, err)

resolver := `function(conflict) {var mergedDoc = new Object();
resolver := `function(conflict) {var mergedDoc = new Object();
mergedDoc._cv = "@";
return mergedDoc;}` // invalid - setting cv to something that doesn't match either doc
customConflictResolver, err := db.NewCustomConflictResolver(ctx1, resolver, rt1.GetDatabase().Options.JavascriptTimeout)
Expand Down Expand Up @@ -2119,6 +2119,105 @@ func TestActiveReplicatorConflictRemoveCVFromCache(t *testing.T) {
})
}

func TestActiveReplicatorV4DefaultResolverWithTombstoneLocal(t *testing.T) {
base.RequireNumTestBuckets(t, 2)

sgrRunner := rest.NewSGRTestRunner(t)
// v4 protocol only test
sgrRunner.RunSubprotocolV4(func(t *testing.T) {
activeRT, passiveRT, remoteURLString := sgrRunner.SetupSGRPeers(t)

docID := rest.SafeDocumentName(t, t.Name())
rt1Version := activeRT.PutDoc(docID, `{"source":"activeRT","channels":["alice"]}`)
// delete local version
rt1Version = activeRT.DeleteDoc(docID, rt1Version)
activeRT.WaitForPendingChanges()
// create conflicting update on passiveRT
rt2Version := passiveRT.PutDoc(docID, `{"source":"passiveRT","channels":["alice"]}`)
rt2Version = passiveRT.UpdateDoc(docID, rt2Version, `{"source":"passiveRT-updated","channels":["alice"]}`)
passiveRT.WaitForPendingChanges()

replicationID := rest.SafeDocumentName(t, t.Name())
activeRT.CreateReplication(replicationID, remoteURLString, db.ActiveReplicatorTypePushAndPull, nil, true, db.ConflictResolverDefault, "")
activeRT.WaitForReplicationStatus(replicationID, db.ReplicationStateRunning)

require.EventuallyWithT(t, func(c *assert.CollectT) {
replicationStats, err := activeRT.GetDatabase().DbStats.DBReplicatorStats(replicationID)
assert.NoError(c, err)
assert.Equal(c, int64(1), replicationStats.ConflictResolvedLocalCount.Value())
}, 10*time.Second, 100*time.Millisecond)

docBodyBytes := []byte(`{}`)
newRev := getRevTreeID(t, rt2Version.RevTreeID, docBodyBytes)
conflictResVersion := rt1Version
conflictResVersion.RevTreeID = newRev

// expect local doc to win and still be tombstone with remote docs revision history
sgrRunner.WaitForTombstone(docID, activeRT, conflictResVersion)

rt1Doc := activeRT.GetDocument(docID)
// assert that remote cv is in pv now
assert.Equal(t, rt2Version.CV.Value, rt1Doc.HLV.PreviousVersions[rt2Version.CV.SourceID])
// assert doc is still a tombstone
assert.True(t, rt1Doc.IsDeleted())
// assert on rev tree structure
docHistoryLeaves := rt1Doc.History.GetLeaves()
require.Len(t, docHistoryLeaves, 2)
for _, revID := range docHistoryLeaves {
assert.True(t, rt1Doc.History[revID].Deleted)
}
})
}

func TestActiveReplicatorV4DefaultResolverWithTombstoneRemote(t *testing.T) {
base.RequireNumTestBuckets(t, 2)

sgrRunner := rest.NewSGRTestRunner(t)
// v4 protocol only test
sgrRunner.RunSubprotocolV4(func(t *testing.T) {
activeRT, passiveRT, remoteURLString := sgrRunner.SetupSGRPeers(t)
docID := rest.SafeDocumentName(t, t.Name())

rt2Version := passiveRT.PutDoc(docID, `{"source":"passiveRT","channels":["alice"]}`)
rt2Version = passiveRT.DeleteDoc(docID, rt2Version)
passiveRT.WaitForPendingChanges()
// create conflicting update on activeRT
rt1Version := activeRT.PutDoc(docID, `{"source":"activeRT","channels":["alice"]}`)
// delete local version
rt1Version = activeRT.UpdateDoc(docID, rt1Version, `{"source":"activeRT-updated","channels":["alice"]}`)
activeRT.WaitForPendingChanges()

replicationID := rest.SafeDocumentName(t, t.Name())
activeRT.CreateReplication(replicationID, remoteURLString, db.ActiveReplicatorTypePushAndPull, nil, true, db.ConflictResolverDefault, "")
activeRT.WaitForReplicationStatus(replicationID, db.ReplicationStateRunning)

require.EventuallyWithT(t, func(c *assert.CollectT) {
replicationStats, err := activeRT.GetDatabase().DbStats.DBReplicatorStats(replicationID)
assert.NoError(c, err)
assert.Equal(c, int64(1), replicationStats.ConflictResolvedRemoteCount.Value())
}, 10*time.Second, 100*time.Millisecond)

// expect remote doc to win but local doc ends up with longer history and given both local and remote branches
// are tombstoned then we end up moving local rev 3-xyz to be the current rev
newRev := getRevTreeID(t, rt1Version.RevTreeID, []byte(db.DeletedDocument))
conflictResVersion := rt2Version
conflictResVersion.RevTreeID = newRev
sgrRunner.WaitForTombstone(docID, activeRT, conflictResVersion)

rt1Doc := activeRT.GetDocument(docID)
// assert that remote cv is in pv now
assert.Equal(t, rt1Version.CV.Value, rt1Doc.HLV.PreviousVersions[rt1Version.CV.SourceID])
// assert doc is still a tombstone
assert.True(t, rt1Doc.IsDeleted())
// assert on rev tree structure
docHistoryLeaves := rt1Doc.History.GetLeaves()
require.Len(t, docHistoryLeaves, 2)
for _, revID := range docHistoryLeaves {
assert.True(t, rt1Doc.History[revID].Deleted)
}
})
}

// getRevTreeID create a revtree ID for a new revision that is a child of the parentRevID for a given body.
func getRevTreeID(t *testing.T, parentRevID string, body []byte) string {
prevGeneration, _ := db.ParseRevID(t.Context(), parentRevID)
Expand Down
4 changes: 0 additions & 4 deletions rest/replicatortest/replicator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6081,8 +6081,6 @@ func TestSGR2TombstoneConflictHandling(t *testing.T) {
func TestDefaultConflictResolverWithTombstoneLocal(t *testing.T) {
base.LongRunningTest(t)

// CBG-4799: tests wil be refactored to allow for easier switch between rev tree and version vector

base.RequireNumTestBuckets(t, 2)
if !base.TestUseXattrs() {
t.Skip("This test only works with XATTRS enabled")
Expand Down Expand Up @@ -6209,8 +6207,6 @@ func TestDefaultConflictResolverWithTombstoneLocal(t *testing.T) {
func TestDefaultConflictResolverWithTombstoneRemote(t *testing.T) {
base.LongRunningTest(t)

// CBG-4799: tests wil be refactored to allow for easier switch between rev tree and version vector

base.RequireNumTestBuckets(t, 2)
if !base.TestUseXattrs() {
t.Skip("This test only works with XATTRS enabled")
Expand Down
Loading