Skip to content
Open
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
29 changes: 22 additions & 7 deletions rest/utilities_testing_blip_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -339,25 +339,36 @@ func (cd *clientDoc) _hasConflict(t testing.TB, incomingHLV *db.HybridLogicalVec
return false
}

func (btcc *BlipTesterCollectionClient) _resolveConflict(incomingHLV *db.HybridLogicalVector, incomingBody []byte, localDoc *clientDocRev) (body []byte, hlv db.HybridLogicalVector) {
func (btcc *BlipTesterCollectionClient) _resolveConflict(incomingHLV *db.HybridLogicalVector, incomingBody []byte, incomingIsDelete bool, localDoc *clientDocRev) (body []byte, hlv db.HybridLogicalVector, isTombstone bool) {
switch btcc.parent.ConflictResolver {
case ConflictResolverLastWriteWins:
return btcc._resolveConflictLWW(incomingHLV, incomingBody, localDoc)
return btcc._resolveConflictLWW(incomingHLV, incomingBody, incomingIsDelete, localDoc)
}
btcc.TB().Fatalf("Unknown conflict resolver %q - cannot resolve detected conflict", btcc.parent.ConflictResolver)
return nil, db.HybridLogicalVector{}
return nil, db.HybridLogicalVector{}, isTombstone
}

func (btcc *BlipTesterCollectionClient) _resolveConflictLWW(incomingHLV *db.HybridLogicalVector, incomingBody []byte, latestLocalRev *clientDocRev) (body []byte, hlv db.HybridLogicalVector) {
func (btcc *BlipTesterCollectionClient) _resolveConflictLWW(incomingHLV *db.HybridLogicalVector, incomingBody []byte, incomingIsDelete bool, latestLocalRev *clientDocRev) (body []byte, hlv db.HybridLogicalVector, isTombstone bool) {
latestLocalHLV := latestLocalRev.HLV
updatedHLV := latestLocalRev.HLV.Copy()
localDeleted := latestLocalRev.isDelete
if localDeleted && !incomingIsDelete {
// resolve in favour of local document
incomingHLV.UpdateWithIncomingHLV(updatedHLV)
return latestLocalRev.body, *updatedHLV, true
}
if incomingIsDelete && !localDeleted {
// resolve in favour of remote document
updatedHLV.UpdateWithIncomingHLV(incomingHLV)
return incomingBody, *updatedHLV, true
}
// resolve conflict in favor of remote document
if incomingHLV.Version > latestLocalHLV.Version {
updatedHLV.UpdateWithIncomingHLV(incomingHLV)
return incomingBody, *updatedHLV
return incomingBody, *updatedHLV, incomingIsDelete
}
incomingHLV.UpdateWithIncomingHLV(updatedHLV)
return latestLocalRev.body, *updatedHLV
return latestLocalRev.body, *updatedHLV, latestLocalRev.isDelete
}

type BlipTesterCollectionClient struct {
Expand Down Expand Up @@ -2180,14 +2191,18 @@ func (btcc *BlipTesterCollectionClient) addRev(ctx context.Context, docID string
btcc.seqLock.Lock()
defer btcc.seqLock.Unlock()
newClientSeq := btcc._nextSequence()
isTombstone := false

newBody := opts.body
newVersion := opts.incomingVersion
doc, hasLocalDoc := btcc._getClientDoc(docID)
updatedHLV := doc._getLatestHLVCopy(btcc.TB())
require.NotNil(btcc.TB(), updatedHLV, "updatedHLV should not be nil for docID %q", docID)
if doc._hasConflict(btcc.TB(), opts.incomingHLV) {
newBody, updatedHLV = btcc._resolveConflict(opts.incomingHLV, opts.body, doc._latestRev(btcc.TB()))
newBody, updatedHLV, isTombstone = btcc._resolveConflict(opts.incomingHLV, opts.body, opts.isDelete, doc._latestRev(btcc.TB()))
if isTombstone {
opts.isDelete = true
}
base.DebugfCtx(ctx, base.KeySGTest, "Resolved conflict for docID %q, incomingHLV:%#v, existingHLV:%#v, updatedHLV:%#v", docID, opts.incomingHLV, doc._latestRev(btcc.TB()).HLV, updatedHLV)
} else {
base.DebugfCtx(ctx, base.KeySGTest, "No conflict")
Expand Down
17 changes: 15 additions & 2 deletions topologytest/multi_actor_no_conflict_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ package topologytest

import (
"fmt"
"strings"
"testing"

"github.com/couchbase/sync_gateway/base"
Expand Down Expand Up @@ -106,9 +107,21 @@ func TestMultiActorResurrect(t *testing.T) {

resBody := fmt.Appendf(nil, `{"activePeer": "%s", "createPeer": "%s", "deletePeer": "%s", "resurrectPeer": "%s", "topology": "%s", "action": "resurrect"}`, resurrectPeerName, createPeerName, deletePeer, resurrectPeer, topology.specDescription)
resurrectVersion := resurrectPeer.WriteDocument(collectionName, docID, resBody)
// in the case of a Couchbase Server resurrection, the hlv is lost since all system xattrs are lost on a resurrection
// in the case of a Couchbase Server resurrection, the hlv is lost since all system xattrs are
// lost on a resurrection so the resurrecting version may conflict with a version on cbl
// peer then cbl will resolve in favor if its own tombstone.
if resurrectPeer.Type() == PeerTypeCouchbaseServer {
waitForCVAndBody(t, collectionName, docID, resurrectVersion, topology)
if strings.Contains(topologySpec.description, "CBL") {
if conflictNotExpectedOnCBL(deletePeer, resurrectPeer, deletePeerName, resurrectPeerName) {
// if no cbl conflict is expected we can wait on CV and body
waitForCVAndBody(t, collectionName, docID, resurrectVersion, topology)
} else {
// if cbl conflict is expected we need to wait for tombstone convergence
waitForConvergingTombstones(t, collectionName, docID, topology)
}
} else {
waitForCVAndBody(t, collectionName, docID, resurrectVersion, topology)
}
} else {
waitForVersionAndBody(t, collectionName, docID, resurrectVersion, topology)
}
Expand Down
28 changes: 28 additions & 0 deletions topologytest/peer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,34 @@ func (p Peers) SortedPeers() iter.Seq2[string, Peer] {
}
}

// peerIsServerSide returns true if the peer is a Couchbase Server or Sync Gateway peer.
func peerIsServerSide(p Peer) bool {
return p.Type() == PeerTypeCouchbaseServer || p.Type() == PeerTypeSyncGateway
}

// conflictNotExpectedOnCBL will return true if no conflict is expected for delete and resurrect operations for
// topologies with cbl peer in them and false for expected conflict
func conflictNotExpectedOnCBL(deletePeer Peer, resurrectPeer Peer, delPeerName string, resPeerName string) bool {
if delPeerName == "cbl1" {
// cbl delete will mean cbs resurrect has a conflict
Comment on lines +138 to +139
Copy link

Copilot AI Dec 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] Consider using strings.Contains(delPeerName, \"cbl\") or a constant for "cbl1" to avoid hardcoding the peer name and improve maintainability.

Suggested change
if delPeerName == "cbl1" {
// cbl delete will mean cbs resurrect has a conflict
// If the delete peer is a Couchbase Lite peer, conflict is expected.
if strings.HasPrefix(delPeerName, "cbl") {

Copilot uses AI. Check for mistakes.
return false
}
if peerIsServerSide(deletePeer) && peerIsServerSide(resurrectPeer) {
if strings.Contains(delPeerName, "1") && strings.Contains(resPeerName, "2") {
// conflict expected due to different backing bucket (sourceID)
return false
}
if strings.Contains(delPeerName, "2") && strings.Contains(resPeerName, "1") {
// conflict expected due to different backing bucket (sourceID)
return false
}
// if both actors are server side and same backing bucket, no conflict expected
return true
}
// conflict expected
return false
}

// NonImportSortedPeers returns a sorted iterator peers that will not cause import operations. For example:
// - cbs1 <-> sg1 <-> cbl1 would return sg1 and cbl1, but not cbs1
// - cbs1 <-> cbs2 would return cbs1 and cbs2
Expand Down