Skip to content

Commit

Permalink
CBG-4430: fix conflict on hlv comparison when parent rev is in history
Browse files Browse the repository at this point in the history
  • Loading branch information
gregns1 committed Dec 18, 2024
1 parent 0da6692 commit 9819b57
Show file tree
Hide file tree
Showing 2 changed files with 127 additions and 36 deletions.
64 changes: 47 additions & 17 deletions db/crud.go
Original file line number Diff line number Diff line change
Expand Up @@ -1228,6 +1228,9 @@ func (db *DatabaseCollectionWithUser) PutExistingCurrentVersion(ctx context.Cont
prevGeneration, _ = ParseRevID(ctx, previousRevTreeID)
newGeneration = prevGeneration + 1
}
revTreeConflictChecked := false
var parent string
var currentRevIndex int

// Conflict check here
// if doc has no HLV defined this is a new doc we haven't seen before, skip conflict check
Expand All @@ -1249,29 +1252,35 @@ func (db *DatabaseCollectionWithUser) PutExistingCurrentVersion(ctx context.Cont
return nil, nil, false, nil, addNewerVersionsErr
}
} else {
base.InfofCtx(ctx, base.KeyCRUD, "conflict detected between the two HLV's for doc %s, incoming version %s, local version %s", base.UD(doc.ID), newDocHLV.GetCurrentVersionString(), doc.HLV.GetCurrentVersionString())
// cancel rest of update, HLV needs to be sent back to client with merge versions populated
return nil, nil, false, nil, base.HTTPErrorf(http.StatusConflict, "Document revision conflict")
if len(revTreeHistory) > 0 {
// conflict check on rev tree history, if there is a rev in rev tree history we have the parent of locally we are not in conflict
parent, currentRevIndex, err = db.revTreeConflictCheck(ctx, revTreeHistory, doc, newDoc.Deleted)
if err != nil {
return nil, nil, false, nil, err
}
revTreeConflictChecked = true
addNewerVersionsErr := doc.HLV.AddNewerVersions(newDocHLV)
if addNewerVersionsErr != nil {
return nil, nil, false, nil, addNewerVersionsErr
}
} else {
base.InfofCtx(ctx, base.KeyCRUD, "conflict detected between the two HLV's for doc %s, incoming version %s, local version %s", base.UD(doc.ID), newDocHLV.GetCurrentVersionString(), doc.HLV.GetCurrentVersionString())
// cancel rest of update, HLV needs to be sent back to client with merge versions populated
return nil, nil, false, nil, base.HTTPErrorf(http.StatusConflict, "Document revision conflict")
}
}
}
// populate merge versions
if newDocHLV.MergeVersions != nil {
doc.HLV.MergeVersions = newDocHLV.MergeVersions
}
// rev tree conflict check if we have rev tree history to check against
currentRevIndex := len(revTreeHistory)
parent := ""
if currentRevIndex > 0 {
for i, revid := range revTreeHistory {
if doc.History.contains(revid) {
currentRevIndex = i
parent = revid
break
}
}
// conflict check on rev tree history
if db.IsIllegalConflict(ctx, doc, parent, newDoc.Deleted, true, revTreeHistory) {
return nil, nil, false, nil, base.HTTPErrorf(http.StatusConflict, "Document revision conflict")
// rev tree conflict check if we have rev tree history to check against + finds current rev index to allow us
// to add any new revision to rev tree below.
// Only check for rev tree conflicts if we haven't already checked above
if !revTreeConflictChecked {
parent, currentRevIndex, err = db.revTreeConflictCheck(ctx, revTreeHistory, doc, newDoc.Deleted)
if err != nil {
return nil, nil, false, nil, err
}
}
// Add all the new revisions to the rev tree:
Expand Down Expand Up @@ -1560,6 +1569,27 @@ func (db *DatabaseCollectionWithUser) SyncFnDryrun(ctx context.Context, body Bod
return output, nil, err
}

// revTreeConflictCheck checks for conflicts in the rev tree history and returns the parent revid, currentRevIndex
// (index of parent rev), and an error if the document is in conflict
func (db *DatabaseCollectionWithUser) revTreeConflictCheck(ctx context.Context, revTreeHistory []string, doc *Document, newDocDeleted bool) (string, int, error) {
currentRevIndex := len(revTreeHistory)
parent := ""
if currentRevIndex > 0 {
for i, revid := range revTreeHistory {
if doc.History.contains(revid) {
currentRevIndex = i
parent = revid
break
}
}
// conflict check on rev tree history
if db.IsIllegalConflict(ctx, doc, parent, newDocDeleted, true, revTreeHistory) {
return "", 0, base.HTTPErrorf(http.StatusConflict, "Document revision conflict")
}
}
return parent, currentRevIndex, nil
}

// resolveConflict runs the conflictResolverFunction with doc and newDoc. doc and newDoc's bodies and revision trees
// may be changed based on the outcome of conflict resolution - see resolveDocLocalWins, resolveDocRemoteWins and
// resolveDocMerge for specifics on what is changed under each scenario.
Expand Down
99 changes: 80 additions & 19 deletions rest/blip_legacy_revid_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,8 @@ func TestProcessLegacyRev(t *testing.T) {
// - 2. CBL sends rev=1010@CBL1, history=1000@CBL2,1-abc when SGW has current rev 1-abc (document underwent multiple p2p updates before being pushed to SGW)
// - 3. CBL sends rev=1010@CBL1, history=1000@CBL2,2-abc,1-abc when SGW has current rev 1-abc (document underwent multiple legacy and p2p updates before being pushed to SGW)
// - 4. CBL sends rev=1010@CBL1, history=1-abc when SGW does not have the doc (document underwent multiple legacy and p2p updates before being pushed to SGW)
// - 5. CBL sends rev=1010@CBL1, history=2-abc and SGW has 1000@CBL2, 2-abc
// - 6. CBL sends rev=1010@CBL1, history=3-abc,2-abc,1-abc and SGW has 1000@SGW, 1-abc
// - Assert that the bucket doc resulting on each operation is as expected
func TestProcessRevWithLegacyHistory(t *testing.T) {
base.SetUpTestLogging(t, base.LevelDebug, base.KeyHTTP, base.KeySync, base.KeySyncMsg)
Expand All @@ -247,6 +249,8 @@ func TestProcessRevWithLegacyHistory(t *testing.T) {
docID2 = "doc2"
docID3 = "doc3"
docID4 = "doc4"
docID5 = "doc5"
docID6 = "doc6"
)

// 1. CBL sends rev=1010@CBL1, history=1-abc when SGW has current rev 1-abc (document underwent an update before being pushed to SGW)
Expand Down Expand Up @@ -323,6 +327,57 @@ func TestProcessRevWithLegacyHistory(t *testing.T) {
assert.Equal(t, "1010@CBL1", bucketDoc.HLV.GetCurrentVersionString())
assert.Equal(t, uint64(4096), bucketDoc.HLV.PreviousVersions["CBL2"])
assert.NotNil(t, bucketDoc.History["1-abc"])

// 5. CBL sends rev=1010@CBL1, history=2-abc and SGW has 1000@CBL2, 2-abc
// although HLV's are in conflict, this should pass conflict check as local current rev is parent of incoming rev
docVersion = rt.PutDocDirectly(docID5, db.Body{"test": "doc"})

docVersion = rt.UpdateDocDirectly(docID5, docVersion, db.Body{"some": "update"})
version := docVersion.CV.Value
rev2ID := docVersion.RevTreeID
pushedRev := db.Version{
Value: version + 1000,
SourceID: "CBL1",
}

history = []string{rev2ID}
sent, _, _, err = bt.SendRevWithHistory(docID5, pushedRev.String(), history, []byte(`{"some": "update"}`), blip.Properties{})
assert.True(t, sent)
require.NoError(t, err)

// assert that the bucket doc is as expected
bucketDoc, _, err = collection.GetDocWithXattrs(ctx, docID5, db.DocUnmarshalAll)
require.NoError(t, err)
assert.Equal(t, pushedRev.String(), bucketDoc.HLV.GetCurrentVersionString())
assert.Equal(t, docVersion.CV.Value, bucketDoc.HLV.PreviousVersions[docVersion.CV.SourceID])
assert.NotNil(t, bucketDoc.History[rev2ID])

// 6. CBL sends rev=1010@CBL1, history=3-abc,2-abc,1-abc and SGW has 1000@SGW, 1-abc
// replicates the following:
// - a new doc being created on SGW 4.0,
// - a pre 4.0 client pulling this doc on one shot replication
// - then this doc being updated a couple of times on client before client gets upgraded to 4.0
// - after the upgrade client updates it again and pushes to SGW
docVersion = rt.PutDocDirectly(docID6, db.Body{"test": "doc"})
rev1ID = docVersion.RevTreeID

pushedRev = db.Version{
Value: version + 1000,
SourceID: "CBL1",
}
history = []string{"3-abc", "2-abc", rev1ID}
sent, _, _, err = bt.SendRevWithHistory(docID6, pushedRev.String(), history, []byte(`{"some": "update"}`), blip.Properties{})
assert.True(t, sent)
require.NoError(t, err)

// assert that the bucket doc is as expected
bucketDoc, _, err = collection.GetDocWithXattrs(ctx, docID6, db.DocUnmarshalAll)
require.NoError(t, err)
assert.Equal(t, pushedRev.String(), bucketDoc.HLV.GetCurrentVersionString())
assert.Equal(t, docVersion.CV.Value, bucketDoc.HLV.PreviousVersions[docVersion.CV.SourceID])
assert.NotNil(t, bucketDoc.History[rev1ID])
assert.NotNil(t, bucketDoc.History["2-abc"])
assert.NotNil(t, bucketDoc.History["3-abc"])
}

// TestProcessRevWithLegacyHistoryConflict:
Expand Down Expand Up @@ -395,22 +450,6 @@ func TestProcessRevWithLegacyHistoryConflict(t *testing.T) {
sent, _, _, err = bt.SendRevWithHistory(docID3, "1010@CBL1", history, []byte(`{"some": "update"}`), blip.Properties{})
assert.True(t, sent)
require.ErrorContains(t, err, "Document revision conflict")

// 4. CBL sends rev=1010@CBL1, history=2-abc and SGW has 1000@CBL2, 2-abc
docVersion = rt.PutDocDirectly(docID4, db.Body{"test": "doc"})

docVersion = rt.UpdateDocDirectly(docID4, docVersion, db.Body{"some": "update"})
version := docVersion.CV.Value
rev2ID = docVersion.RevTreeID
pushedRev := db.Version{
Value: version + 1000,
SourceID: "CBL1",
}

history = []string{rev2ID}
sent, _, _, err = bt.SendRevWithHistory(docID4, pushedRev.String(), history, []byte(`{"some": "update"}`), blip.Properties{})
assert.True(t, sent)
require.ErrorContains(t, err, "Document revision conflict")
}

// TestChangesResponseLegacyRev:
Expand Down Expand Up @@ -821,6 +860,8 @@ func TestPushDocConflictBetweenPreUpgradeCBLMutationAndPostUpgradeSGWMutation(t

// TestConflictBetweenPostUpgradeCBLMutationAndPostUpgradeSGWMutation:
// - Test case 6 of conflict test plan from design doc
// - First sent rev will not conflict as current local rev is parent of incoming rev
// - Second sent rev will conflict as incoming rev has no common ancestor with local rev and HLV's are in conflict
func TestConflictBetweenPostUpgradeCBLMutationAndPostUpgradeSGWMutation(t *testing.T) {
base.SetUpTestLogging(t, base.LevelDebug, base.KeyHTTP, base.KeySync, base.KeySyncMsg, base.KeyChanges)

Expand All @@ -833,17 +874,37 @@ func TestConflictBetweenPostUpgradeCBLMutationAndPostUpgradeSGWMutation(t *testi
defer bt.Close()
rt := bt.restTester
collection, ctx := rt.GetSingleTestDatabaseCollection()
const (
docID = "doc1"
docID2 = "doc2"
)

docVersion := rt.PutDocDirectly("doc1", db.Body{"test": "doc"})
docVersion := rt.PutDocDirectly(docID, db.Body{"test": "doc"})
rev1ID := docVersion.RevTreeID

history := []string{rev1ID}
sent, _, _, err := bt.SendRevWithHistory("doc1", "100@CBL1", history, []byte(`{"key": "val"}`), blip.Properties{})
sent, _, _, err := bt.SendRevWithHistory(docID, "100@CBL1", history, []byte(`{"key": "val"}`), blip.Properties{})
assert.True(t, sent)
require.NoError(t, err)

// assert that the bucket doc is as expected
bucketDoc, _, err := collection.GetDocWithXattrs(ctx, docID, db.DocUnmarshalAll)
require.NoError(t, err)
assert.Equal(t, "100@CBL1", bucketDoc.HLV.GetCurrentVersionString())
assert.NotNil(t, bucketDoc.History[rev1ID])
assert.Equal(t, docVersion.CV.Value, bucketDoc.HLV.PreviousVersions[docVersion.CV.SourceID])

// conflict rev
docVersion = rt.PutDocDirectly(docID2, db.Body{"some": "doc"})
rev1ID = docVersion.RevTreeID

history = []string{"1-abc"}
sent, _, _, err = bt.SendRevWithHistory(docID2, "100@CBL1", history, []byte(`{"key": "val"}`), blip.Properties{})
assert.True(t, sent)
require.ErrorContains(t, err, "Document revision conflict")

// assert that the bucket doc is as expected
bucketDoc, _, err := collection.GetDocWithXattrs(ctx, "doc1", db.DocUnmarshalAll)
bucketDoc, _, err = collection.GetDocWithXattrs(ctx, docID2, db.DocUnmarshalAll)
require.NoError(t, err)
assert.Equal(t, rev1ID, bucketDoc.CurrentRev)
assert.Equal(t, docVersion.CV.String(), bucketDoc.HLV.GetCurrentVersionString())
Expand Down

0 comments on commit 9819b57

Please sign in to comment.