diff --git a/rest/utilities_testing_blip_client.go b/rest/utilities_testing_blip_client.go index cbe748de0f..aa890add49 100644 --- a/rest/utilities_testing_blip_client.go +++ b/rest/utilities_testing_blip_client.go @@ -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 { @@ -2180,6 +2191,7 @@ 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 @@ -2187,7 +2199,10 @@ func (btcc *BlipTesterCollectionClient) addRev(ctx context.Context, docID string 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") diff --git a/topologytest/multi_actor_no_conflict_test.go b/topologytest/multi_actor_no_conflict_test.go index ae6ce87870..81480f61b1 100644 --- a/topologytest/multi_actor_no_conflict_test.go +++ b/topologytest/multi_actor_no_conflict_test.go @@ -10,6 +10,7 @@ package topologytest import ( "fmt" + "strings" "testing" "github.com/couchbase/sync_gateway/base" @@ -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) } diff --git a/topologytest/peer_test.go b/topologytest/peer_test.go index 3cfbbe564f..d7f7f90f02 100644 --- a/topologytest/peer_test.go +++ b/topologytest/peer_test.go @@ -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 + 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