Skip to content

Commit 39c65d7

Browse files
authored
CBG-4806: move conflict detection to common function (#7683)
1 parent 82abaad commit 39c65d7

File tree

4 files changed

+107
-26
lines changed

4 files changed

+107
-26
lines changed

db/crud.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1298,6 +1298,7 @@ func (db *DatabaseCollectionWithUser) PutExistingCurrentVersion(ctx context.Cont
12981298
revTreeConflictChecked := false
12991299
var parent string
13001300
var currentRevIndex int
1301+
var isConflict bool
13011302

13021303
// Conflict check here
13031304
// if doc has no HLV defined this is a new doc we haven't seen before, skip conflict check
@@ -1314,11 +1315,12 @@ func (db *DatabaseCollectionWithUser) PutExistingCurrentVersion(ctx context.Cont
13141315
}
13151316
}
13161317
} else {
1317-
if doc.HLV.isDominating(newDocHLV) {
1318+
isConflict, err = IsInConflict(ctx, doc.HLV, newDocHLV)
1319+
if err != nil && errors.Is(err, ErrNoNewVersionsToAdd) {
13181320
base.DebugfCtx(ctx, base.KeyCRUD, "PutExistingCurrentVersion(%q): No new versions to add. existing: %#v new:%#v", base.UD(newDoc.ID), doc.HLV, newDocHLV)
13191321
return nil, nil, false, nil, base.ErrUpdateCancel // No new revisions to add
13201322
}
1321-
if newDocHLV.isDominating(doc.HLV) {
1323+
if !isConflict {
13221324
// update hlv for all newer incoming source version pairs
13231325
addNewerVersionsErr := doc.HLV.AddNewerVersions(newDocHLV)
13241326
if addNewerVersionsErr != nil {

db/hybrid_logical_vector.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@
99
package db
1010

1111
import (
12+
"context"
1213
"encoding/base64"
14+
"errors"
1315
"fmt"
1416
"maps"
1517
"sort"
@@ -689,3 +691,51 @@ func (hlv *HybridLogicalVector) UnmarshalJSON(inputjson []byte) error {
689691
func (hlv HybridLogicalVector) GoString() string {
690692
return fmt.Sprintf("HybridLogicalVector{CurrentVersionCAS:%d, SourceID:%s, Version:%d, PreviousVersions:%#+v, MergeVersions:%#+v}", hlv.CurrentVersionCAS, hlv.SourceID, hlv.Version, hlv.PreviousVersions, hlv.MergeVersions)
691693
}
694+
695+
// ErrNoNewVersionsToAdd will be thrown when there are no new versions from incoming HLV to be added to HLV that is local
696+
var ErrNoNewVersionsToAdd = errors.New("no new versions to add to HLV")
697+
698+
// IsInConflict is used to identify if two HLV's are in conflict or not. Will return boolean to indicate if in conflict
699+
// or not and will error for the following cases:
700+
// - Local HLV dominates incoming HLV (meaning local version is a newer version that the incoming one)
701+
// - Local CV matches incoming CV, so no new versions to add
702+
func IsInConflict(ctx context.Context, localHLV, incomingHLV *HybridLogicalVector) (bool, error) {
703+
incomingCV := incomingHLV.ExtractCurrentVersionFromHLV()
704+
localCV := localHLV.ExtractCurrentVersionFromHLV()
705+
706+
// check if incoming CV and local CV are the same. This is needed here given that if both CV's are the same the
707+
// below check to check local revision is newer than incoming revision will pass given the check and will add the
708+
// incoming versions even if CVs are the same
709+
if localCV.Equal(*incomingCV) {
710+
base.TracefCtx(ctx, base.KeyVV, "incoming CV %#+v is equal to local revision %#+v", incomingCV, localCV)
711+
return false, ErrNoNewVersionsToAdd
712+
}
713+
714+
// standard no conflict case. In the simple case, this happens when:
715+
// - Client A writes document 1@srcA
716+
// - Client B pulls document 1@srcA from Client A
717+
// - Client A writes document 2@srcA
718+
// - Client B pulls document 2@srcA from Client A
719+
if incomingHLV.DominatesSource(*localCV) {
720+
return false, nil
721+
}
722+
723+
// local revision is newer than incoming revision. Common case:
724+
// - Client A writes document 1@srcA
725+
// - Client A pushes to Client B as 1@srcA
726+
// - Client A pulls document 1@srcA from Client B
727+
//
728+
// NOTE: without P2P replication, this should not be the case and we would not get this revision, since Client A
729+
// would respond to a Client B changes message that Client A does not need this revision
730+
if localHLV.DominatesSource(*incomingCV) {
731+
return false, ErrNoNewVersionsToAdd
732+
}
733+
// Check if conflict has been previously resolved.
734+
// - If merge versions are empty, then it has not be resolved.
735+
// - If merge versions do not match, then it has not been resolved.
736+
if len(incomingHLV.MergeVersions) != 0 && len(localHLV.MergeVersions) != 0 && maps.Equal(incomingHLV.MergeVersions, localHLV.MergeVersions) {
737+
base.DebugfCtx(ctx, base.KeyVV, "merge versions match between local HLV %#v and incoming HLV %#v, conflict previously resolved", localHLV, incomingCV)
738+
return false, nil
739+
}
740+
return true, nil
741+
}

db/hybrid_logical_vector_test.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1413,3 +1413,52 @@ func TestAddVersion(t *testing.T) {
14131413
})
14141414
}
14151415
}
1416+
1417+
func TestIsInConflict(t *testing.T) {
1418+
testCases := []struct {
1419+
name string
1420+
localHLV string
1421+
incomingHLV string
1422+
expectedInConflict bool
1423+
expectedError bool
1424+
}{
1425+
{
1426+
name: "CV equal",
1427+
localHLV: "111@abc;123@def",
1428+
incomingHLV: "111@abc;123@ghi",
1429+
expectedError: true,
1430+
},
1431+
{
1432+
name: "no conflict case",
1433+
localHLV: "111@abc;123@def",
1434+
incomingHLV: "112@abc;123@ghi",
1435+
},
1436+
{
1437+
name: "local revision is newer",
1438+
localHLV: "111@abc;123@def",
1439+
incomingHLV: "100@abc;123@ghi",
1440+
expectedError: true,
1441+
},
1442+
{
1443+
name: "merge versions match",
1444+
localHLV: "130@abc,123@def,100@ghi;50@jkl",
1445+
incomingHLV: "150@mno,123@def,100@ghi;50@jkl",
1446+
},
1447+
}
1448+
for _, tc := range testCases {
1449+
t.Run(tc.name, func(t *testing.T) {
1450+
localHLV, _, err := extractHLVFromBlipString(tc.localHLV)
1451+
require.NoError(t, err)
1452+
incomingHLV, _, err := extractHLVFromBlipString(tc.incomingHLV)
1453+
require.NoError(t, err)
1454+
1455+
inConflict, err := IsInConflict(t.Context(), localHLV, incomingHLV)
1456+
if tc.expectedError {
1457+
require.Error(t, err)
1458+
} else {
1459+
require.NoError(t, err)
1460+
}
1461+
require.Equal(t, tc.expectedInConflict, inConflict)
1462+
})
1463+
}
1464+
}

rest/utilities_testing_blip_client.go

Lines changed: 4 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ import (
1616
"encoding/base64"
1717
"fmt"
1818
"iter"
19-
"maps"
2019
"net/http"
2120
"slices"
2221
"strconv"
@@ -325,35 +324,16 @@ func (cd *clientDoc) _hasConflict(t testing.TB, incomingHLV *db.HybridLogicalVec
325324
incomingCV := incomingHLV.ExtractCurrentVersionFromHLV()
326325
localCV := localHLV.ExtractCurrentVersionFromHLV()
327326
// safety check - ensure SG is not sending a rev that we already had - ensures changes feed messaging is working correctly to prevent
327+
// this check is also performed in the function below but we should keep this here for extra context on FailNow call
328328
if localCV.Equal(*incomingCV) {
329329
require.FailNow(t, fmt.Sprintf("incoming CV %#+v is equal to local revision %#+v - this should've been filtered via changes response before ending up as a rev. This is only true if there is a single replication occurring, two simultaneous replications (e.g. P2P) could cause this. If there are multiple replications, modify code.", incomingCV, latestRev))
330330
}
331-
// standard no conflict case. In the simple case, this happens when:
332-
// - SG writes document 1@cbs1
333-
// - CBL pulls document 1@cbs1
334-
// - SG writes document 2@cbs1
335-
if incomingHLV.DominatesSource(*localCV) {
336-
return false
337-
}
338331

339-
// local revision is newer than incoming revision. Common case:
340-
// - CBL writes document 1@cbl1
341-
// - CBL pushes to SG as 1@cbl1
342-
// - CBL pulls document 1@cbl1
343-
//
344-
// NOTE: without P2P replication, this should not be the case and we would not get this revision, since CBL
345-
// would respond to a SG changes message that CBL does not need this revision
346-
if localHLV.DominatesSource(*incomingCV) {
332+
inConflict, err := db.IsInConflict(t.Context(), &localHLV, incomingHLV)
333+
if err != nil {
347334
require.FailNow(t, fmt.Sprintf("incoming CV %#+v has lower version than the local revision %#+v - this should've been filtered via changes response before ending up as a rev. blip tester would reply that to Sync Gateway that it doesn't need this revision", incomingCV, localHLV))
348-
return false
349-
}
350-
// Check if conflict has been previously resolved.
351-
// - If merge versions are empty, then it has not be resolved.
352-
// - If merge versions do not match, then it has not been resolved.
353-
if len(incomingHLV.MergeVersions) != 0 && len(localHLV.MergeVersions) != 0 && maps.Equal(incomingHLV.MergeVersions, localHLV.MergeVersions) {
354-
return false
355335
}
356-
return true
336+
return inConflict
357337
}
358338

359339
func (btcc *BlipTesterCollectionClient) _resolveConflict(incomingHLV *db.HybridLogicalVector, incomingBody []byte, localDoc *clientDocRev) (body []byte, hlv db.HybridLogicalVector) {

0 commit comments

Comments
 (0)