Skip to content

Commit d64fb97

Browse files
authored
[4.0.0 backport] CBG-4903: Consider local/remote tombstones in the default conflict resolver for ISGR (#7804)
1 parent b8d105b commit d64fb97

File tree

3 files changed

+197
-1
lines changed

3 files changed

+197
-1
lines changed

db/hybrid_logical_vector.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -952,6 +952,15 @@ func DefaultLWWConflictResolutionType(ctx context.Context, conflict Conflict) (B
952952
if conflict.LocalHLV == nil || conflict.RemoteHLV == nil {
953953
return nil, errors.New("local or incoming document is nil for resolveConflict")
954954
}
955+
localDeleted := conflict.LocalDocument.IsDeleted()
956+
remoteDeleted := conflict.RemoteDocument.IsDeleted()
957+
if localDeleted && !remoteDeleted {
958+
return conflict.LocalDocument, nil
959+
}
960+
if remoteDeleted && !localDeleted {
961+
return conflict.RemoteDocument, nil
962+
}
963+
955964
// resolve conflict in favor of remote document, remote wins case
956965
if conflict.RemoteHLV.Version > conflict.LocalHLV.Version {
957966
// remote document wins

db/revision.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,12 @@ func (body Body) ExtractExpiry() (uint32, error) {
204204
return exp, nil
205205
}
206206

207+
// IsDeleted returns true if the body contains a _deleted property set to true.
208+
func (body Body) IsDeleted() bool {
209+
deleted, _ := body[BodyDeleted].(bool)
210+
return deleted
211+
}
212+
207213
func (body Body) ExtractDeleted() bool {
208214
deleted, _ := body[BodyDeleted].(bool)
209215
delete(body, BodyDeleted)

rest/replicatortest/replicator_conflict_test.go

Lines changed: 182 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1580,7 +1580,7 @@ func TestActiveReplicatorInvalidCustomResolver(t *testing.T) {
15801580
_, _, _, err = rt2Collection.PutExistingCurrentVersion(rt2Ctx, opts)
15811581
require.NoError(t, err)
15821582

1583-
resolver := `function(conflict) {var mergedDoc = new Object();
1583+
resolver := `function(conflict) {var mergedDoc = new Object();
15841584
mergedDoc._cv = "@";
15851585
return mergedDoc;}` // invalid - setting cv to something that doesn't match either doc
15861586
customConflictResolver, err := db.NewCustomConflictResolver(ctx1, resolver, rt1.GetDatabase().Options.JavascriptTimeout)
@@ -2276,6 +2276,187 @@ func TestActiveReplicatorConflictRemoveCVFromCache(t *testing.T) {
22762276
assert.Equal(t, rt2Version.CV.String(), docRev.HlvHistory)
22772277
}
22782278

2279+
func TestActiveReplicatorV4DefaultResolverWithTombstoneLocal(t *testing.T) {
2280+
base.RequireNumTestBuckets(t, 2)
2281+
2282+
// Passive
2283+
passiveRT := rest.NewRestTester(t, &rest.RestTesterConfig{
2284+
SyncFn: channels.DocChannelsSyncFunction,
2285+
DatabaseConfig: &rest.DatabaseConfig{
2286+
DbConfig: rest.DbConfig{
2287+
Name: "passivedb",
2288+
},
2289+
},
2290+
})
2291+
defer passiveRT.Close()
2292+
username := "alice"
2293+
passiveRT.CreateUser(username, []string{username})
2294+
2295+
// Active
2296+
activeRT := rest.NewRestTester(t, &rest.RestTesterConfig{
2297+
SyncFn: channels.DocChannelsSyncFunction,
2298+
DatabaseConfig: &rest.DatabaseConfig{
2299+
DbConfig: rest.DbConfig{
2300+
Name: "activedb",
2301+
},
2302+
},
2303+
})
2304+
defer activeRT.Close()
2305+
ctx1 := activeRT.Context()
2306+
2307+
docID := rest.SafeDocumentName(t, t.Name())
2308+
rt1Version := activeRT.PutDoc(docID, `{"source":"activeRT","channels":["alice"]}`)
2309+
// delete local version
2310+
rt1Version = activeRT.DeleteDoc(docID, rt1Version)
2311+
activeRT.WaitForPendingChanges()
2312+
// create conflicting update on passiveRT
2313+
rt2Version := passiveRT.PutDoc(docID, `{"source":"passiveRT","channels":["alice"]}`)
2314+
rt2Version = passiveRT.UpdateDoc(docID, rt2Version, `{"source":"passiveRT-updated","channels":["alice"]}`)
2315+
passiveRT.WaitForPendingChanges()
2316+
2317+
replicationID := rest.SafeDocumentName(t, t.Name())
2318+
resolverFunc, err := db.NewConflictResolverFuncForHLV(ctx1, db.ConflictResolverDefault, "", activeRT.GetDatabase().Options.JavascriptTimeout)
2319+
require.NoError(t, err)
2320+
2321+
stats, err := base.SyncGatewayStats.NewDBStats(rest.SafeDocumentName(t, t.Name()), false, false, false, nil, nil)
2322+
require.NoError(t, err)
2323+
replicationStats, err := stats.DBReplicatorStats(replicationID)
2324+
require.NoError(t, err)
2325+
2326+
ar, err := db.NewActiveReplicator(ctx1, &db.ActiveReplicatorConfig{
2327+
ID: replicationID,
2328+
Direction: db.ActiveReplicatorTypePushAndPull,
2329+
RemoteDBURL: userDBURL(passiveRT, username),
2330+
ActiveDB: &db.Database{
2331+
DatabaseContext: activeRT.GetDatabase(),
2332+
},
2333+
ChangesBatchSize: 200,
2334+
ReplicationStatsMap: replicationStats,
2335+
ConflictResolverFuncForHLV: resolverFunc,
2336+
CollectionsEnabled: !activeRT.GetDatabase().OnlyDefaultCollection(),
2337+
Continuous: true,
2338+
})
2339+
require.NoError(t, err)
2340+
defer func() { assert.NoError(t, ar.Stop()) }()
2341+
2342+
require.NoError(t, ar.Start(ctx1))
2343+
2344+
require.EventuallyWithT(t, func(c *assert.CollectT) {
2345+
assert.Equal(c, int64(1), replicationStats.ConflictResolvedLocalCount.Value())
2346+
}, 10*time.Second, 100*time.Millisecond)
2347+
2348+
docBodyBytes := []byte(`{}`)
2349+
newRev := getRevTreeID(t, rt2Version.RevTreeID, docBodyBytes)
2350+
conflictResVersion := rt1Version
2351+
conflictResVersion.RevTreeID = newRev
2352+
2353+
// expect local doc to win and still be tombstone with remote docs revision history
2354+
activeRT.WaitForTombstone(docID, conflictResVersion)
2355+
2356+
rt1Doc := activeRT.GetDocument(docID)
2357+
// assert that remote cv is in pv now
2358+
assert.Equal(t, rt2Version.CV.Value, rt1Doc.HLV.PreviousVersions[rt2Version.CV.SourceID])
2359+
// assert doc is still a tombstone
2360+
assert.True(t, rt1Doc.IsDeleted())
2361+
// assert on rev tree structure
2362+
docHistoryLeaves := rt1Doc.History.GetLeaves()
2363+
require.Len(t, docHistoryLeaves, 2)
2364+
for _, revID := range docHistoryLeaves {
2365+
assert.True(t, rt1Doc.History[revID].Deleted)
2366+
}
2367+
2368+
}
2369+
2370+
func TestActiveReplicatorV4DefaultResolverWithTombstoneRemote(t *testing.T) {
2371+
base.RequireNumTestBuckets(t, 2)
2372+
2373+
// Passive
2374+
passiveRT := rest.NewRestTester(t, &rest.RestTesterConfig{
2375+
SyncFn: channels.DocChannelsSyncFunction,
2376+
DatabaseConfig: &rest.DatabaseConfig{
2377+
DbConfig: rest.DbConfig{
2378+
Name: "passivedb",
2379+
},
2380+
},
2381+
})
2382+
defer passiveRT.Close()
2383+
username := "alice"
2384+
passiveRT.CreateUser(username, []string{username})
2385+
2386+
// Active
2387+
activeRT := rest.NewRestTester(t, &rest.RestTesterConfig{
2388+
SyncFn: channels.DocChannelsSyncFunction,
2389+
DatabaseConfig: &rest.DatabaseConfig{
2390+
DbConfig: rest.DbConfig{
2391+
Name: "activedb",
2392+
},
2393+
},
2394+
})
2395+
defer activeRT.Close()
2396+
ctx1 := activeRT.Context()
2397+
docID := rest.SafeDocumentName(t, t.Name())
2398+
2399+
rt2Version := passiveRT.PutDoc(docID, `{"source":"passiveRT","channels":["alice"]}`)
2400+
rt2Version = passiveRT.DeleteDoc(docID, rt2Version)
2401+
passiveRT.WaitForPendingChanges()
2402+
// create conflicting update on activeRT
2403+
rt1Version := activeRT.PutDoc(docID, `{"source":"activeRT","channels":["alice"]}`)
2404+
// delete local version
2405+
rt1Version = activeRT.UpdateDoc(docID, rt1Version, `{"source":"activeRT-updated","channels":["alice"]}`)
2406+
activeRT.WaitForPendingChanges()
2407+
2408+
replicationID := rest.SafeDocumentName(t, t.Name())
2409+
resolverFunc, err := db.NewConflictResolverFuncForHLV(ctx1, db.ConflictResolverDefault, "", activeRT.GetDatabase().Options.JavascriptTimeout)
2410+
require.NoError(t, err)
2411+
2412+
stats, err := base.SyncGatewayStats.NewDBStats(rest.SafeDocumentName(t, t.Name()), false, false, false, nil, nil)
2413+
require.NoError(t, err)
2414+
replicationStats, err := stats.DBReplicatorStats(replicationID)
2415+
require.NoError(t, err)
2416+
2417+
ar, err := db.NewActiveReplicator(ctx1, &db.ActiveReplicatorConfig{
2418+
ID: replicationID,
2419+
Direction: db.ActiveReplicatorTypePushAndPull,
2420+
RemoteDBURL: userDBURL(passiveRT, username),
2421+
ActiveDB: &db.Database{
2422+
DatabaseContext: activeRT.GetDatabase(),
2423+
},
2424+
ChangesBatchSize: 200,
2425+
ReplicationStatsMap: replicationStats,
2426+
ConflictResolverFuncForHLV: resolverFunc,
2427+
CollectionsEnabled: !activeRT.GetDatabase().OnlyDefaultCollection(),
2428+
Continuous: true,
2429+
})
2430+
require.NoError(t, err)
2431+
defer func() { assert.NoError(t, ar.Stop()) }()
2432+
2433+
require.NoError(t, ar.Start(ctx1))
2434+
2435+
require.EventuallyWithT(t, func(c *assert.CollectT) {
2436+
assert.Equal(c, int64(1), replicationStats.ConflictResolvedRemoteCount.Value())
2437+
}, 10*time.Second, 100*time.Millisecond)
2438+
2439+
// expect remote doc to win but local doc ends up with longer history and given both local and remote branches
2440+
// are tombstoned then we end up moving local rev 3-xyz to be the current rev
2441+
newRev := getRevTreeID(t, rt1Version.RevTreeID, []byte(db.DeletedDocument))
2442+
conflictResVersion := rt2Version
2443+
conflictResVersion.RevTreeID = newRev
2444+
activeRT.WaitForTombstone(docID, conflictResVersion)
2445+
2446+
rt1Doc := activeRT.GetDocument(docID)
2447+
// assert that remote cv is in pv now
2448+
assert.Equal(t, rt1Version.CV.Value, rt1Doc.HLV.PreviousVersions[rt1Version.CV.SourceID])
2449+
// assert doc is still a tombstone
2450+
assert.True(t, rt1Doc.IsDeleted())
2451+
// assert on rev tree structure
2452+
docHistoryLeaves := rt1Doc.History.GetLeaves()
2453+
require.Len(t, docHistoryLeaves, 2)
2454+
for _, revID := range docHistoryLeaves {
2455+
assert.True(t, rt1Doc.History[revID].Deleted)
2456+
}
2457+
2458+
}
2459+
22792460
// getRevTreeID create a revtree ID for a new revision that is a child of the parentRevID for a given body.
22802461
func getRevTreeID(t *testing.T, parentRevID string, body []byte) string {
22812462
prevGeneration, _ := db.ParseRevID(t.Context(), parentRevID)

0 commit comments

Comments
 (0)