Skip to content

Commit f43c0ff

Browse files
authored
CBG-4879: fix for data race when invalidating cache for local wins (#7778)
1 parent 2052ad6 commit f43c0ff

File tree

5 files changed

+122
-4
lines changed

5 files changed

+122
-4
lines changed

db/change_cache.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -517,7 +517,7 @@ func (c *changeCache) DocChanged(event sgbucket.FeedEvent, docType DocumentType)
517517
SourceID: syncData.RevAndVersion.CurrentSource,
518518
Value: base.HexCasToUint64(syncData.RevAndVersion.CurrentVersion),
519519
}
520-
collection.revisionCache.RemoveWithCV(ctx, docID, &vrs)
520+
collection.revisionCache.RemoveCVOnly(ctx, docID, &vrs)
521521
}
522522

523523
change := &LogEntry{

db/revision_cache_bypass.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,10 @@ func (rc *BypassRevisionCache) RemoveRevOnly(ctx context.Context, docID, revID s
132132
// no-op
133133
}
134134

135+
func (rc *BypassRevisionCache) RemoveCVOnly(ctx context.Context, docID string, cv *Version, collectionID uint32) {
136+
// no-op
137+
}
138+
135139
func (rc *BypassRevisionCache) RemoveWithCV(ctx context.Context, docID string, cv *Version, collectionID uint32) {
136140
// no-op
137141
}

db/revision_cache_interface.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,12 @@ type RevisionCache interface {
5555
// RemoveWithCV evicts a revision from the cache using its current version.
5656
RemoveWithCV(ctx context.Context, docID string, cv *Version, collectionID uint32)
5757

58+
// RemoveRevOnly removes the specified key from the revID lookup map in the cache
5859
RemoveRevOnly(ctx context.Context, docID, revID string, collectionID uint32)
5960

61+
// RemoveCVOnly removes the specified key from the HLV lookup map in the cache
62+
RemoveCVOnly(ctx context.Context, docID string, cv *Version, collectionID uint32)
63+
6064
// UpdateDelta stores the given toDelta value in the given rev if cached
6165
UpdateDelta(ctx context.Context, docID, revID string, collectionID uint32, toDelta RevisionDelta)
6266

@@ -178,6 +182,10 @@ func (c *collectionRevisionCache) RemoveRevOnly(ctx context.Context, docID, revI
178182
(*c.revCache).RemoveRevOnly(ctx, docID, revID, c.collectionID)
179183
}
180184

185+
func (c *collectionRevisionCache) RemoveCVOnly(ctx context.Context, docID string, cv *Version) {
186+
(*c.revCache).RemoveCVOnly(ctx, docID, cv, c.collectionID)
187+
}
188+
181189
// RemoveWithCV is for per collection access to Remove method
182190
func (c *collectionRevisionCache) RemoveWithCV(ctx context.Context, docID string, cv *Version) {
183191
(*c.revCache).RemoveWithCV(ctx, docID, cv, c.collectionID)

db/revision_cache_lru.go

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,10 @@ func (sc *ShardedLRURevisionCache) RemoveRevOnly(ctx context.Context, docID, rev
9797
sc.getShard(docID).RemoveRevOnly(ctx, docID, revID, collectionID)
9898
}
9999

100+
func (sc *ShardedLRURevisionCache) RemoveCVOnly(ctx context.Context, docID string, cv *Version, collectionID uint32) {
101+
sc.getShard(docID).RemoveCVOnly(ctx, docID, cv, collectionID)
102+
}
103+
100104
// An LRU cache of document revision bodies, together with their channel access.
101105
type LRURevisionCache struct {
102106
backingStores map[uint32]RevisionCacheBackingStore
@@ -533,6 +537,10 @@ func (rc *LRURevisionCache) RemoveWithCV(ctx context.Context, docID string, cv *
533537
rc.removeFromCacheByCV(ctx, docID, cv, collectionID)
534538
}
535539

540+
func (rc *LRURevisionCache) RemoveCVOnly(ctx context.Context, docID string, cv *Version, collectionID uint32) {
541+
rc.removeFromCVLookup(ctx, docID, cv, collectionID)
542+
}
543+
536544
// RemoveRevOnly removes a rev from revision cache lookup map, if present.
537545
func (rc *LRURevisionCache) RemoveRevOnly(ctx context.Context, docID, revID string, collectionID uint32) {
538546
// This will only remove the entry from the rev lookup map, not the lru list
@@ -587,11 +595,21 @@ func (rc *LRURevisionCache) removeFromRevLookup(ctx context.Context, docID, revI
587595
key := IDAndRev{DocID: docID, RevID: revID, CollectionID: collectionID}
588596
rc.lock.Lock()
589597
defer rc.lock.Unlock()
590-
// only delete from rev lookup map, if we delete underlying element in list, the now elem in the HLV lookup map
598+
// only delete from rev lookup map, if we delete underlying element in list, the elem in the HLV lookup map
591599
// will never be evicted leading to potential unbounded growth of HLV lookup map
592600
delete(rc.cache, key)
593601
}
594602

603+
// removeFromCVLookup will only remove the entry from the CV lookup map, if present. Underlying element must stay in list for eviction to work.
604+
func (rc *LRURevisionCache) removeFromCVLookup(ctx context.Context, docID string, cv *Version, collectionID uint32) {
605+
key := IDandCV{DocID: docID, Source: cv.SourceID, Version: cv.Value, CollectionID: collectionID}
606+
rc.lock.Lock()
607+
defer rc.lock.Unlock()
608+
// only delete from cv lookup map, if we delete underlying element in list, the item left in the revID lookup map for
609+
// this item will never be evicted leading to potential unbounded growth of revID lookup map
610+
delete(rc.hlvCache, key)
611+
}
612+
595613
// removeValue removes a value from the revision cache, if present and the value matches the the value. If there's an item in the revision cache with a matching docID and revID but the document is different, this item will not be removed from the rev cache.
596614
func (rc *LRURevisionCache) removeValue(value *revCacheValue) {
597615
rc.lock.Lock()
@@ -627,7 +645,16 @@ func (rc *LRURevisionCache) _numberCapacityEviction() (numItemsEvicted int64, nu
627645
}
628646
hlvKey := IDandCV{DocID: value.id, Source: value.cv.SourceID, Version: value.cv.Value, CollectionID: value.collectionID}
629647
revKey := IDAndRev{DocID: value.id, RevID: value.revID, CollectionID: value.collectionID}
630-
delete(rc.hlvCache, hlvKey)
648+
if elem := rc.hlvCache[hlvKey]; elem != nil {
649+
revValue := elem.Value.(*revCacheValue)
650+
// we need to check if the value pointed to by the cv lookup map is the same value we're evicting, this is
651+
// because we can currently have two items with the same docID and CV, but different revIDs due to
652+
// local wins conflict resolution not generating a new CV but generating a new revID.
653+
if revValue.revID == value.revID {
654+
// this cv lookup item matches the value we're evicting, so remove it
655+
delete(rc.hlvCache, hlvKey)
656+
}
657+
}
631658
if elem := rc.cache[revKey]; elem != nil {
632659
revValue := elem.Value.(*revCacheValue)
633660
// we need to check if the value pointed to by the rev lookup map is the same value we're evicting, this is
@@ -888,7 +915,17 @@ func (rc *LRURevisionCache) performEviction(ctx context.Context) {
888915
}
889916
revKey := IDAndRev{DocID: value.id, RevID: value.revID, CollectionID: value.collectionID}
890917
hlvKey := IDandCV{DocID: value.id, Source: value.cv.SourceID, Version: value.cv.Value, CollectionID: value.collectionID}
891-
delete(rc.hlvCache, hlvKey)
918+
// same below but for hlv lookup map
919+
if elem := rc.hlvCache[hlvKey]; elem != nil {
920+
revValue := elem.Value.(*revCacheValue)
921+
// we need to check if the value pointed to by the cv lookup map is the same value we're evicting, this is
922+
// because we can currently have two items with the same docID and CV, but different revIDs due to
923+
// local wins conflict resolution not generating a new CV but generating a new revID.
924+
if revValue.revID == value.revID {
925+
// this cv lookup item matches the value we're evicting, so remove it
926+
delete(rc.hlvCache, hlvKey)
927+
}
928+
}
892929
if elem := rc.cache[revKey]; elem != nil {
893930
revValue := elem.Value.(*revCacheValue)
894931
// we need to check if the value pointed to by the rev lookup map is the same value we're evicting, this is

db/revision_cache_test.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2347,3 +2347,72 @@ func TestLoadFromBucketLegacyRevsThatAreBackedUpPreUpgrade(t *testing.T) {
23472347
// 3 misses are from the initial load from bucket after rev cache flush above
23482348
assert.Equal(t, int64(3), db.DbStats.Cache().RevisionCacheMisses.Value())
23492349
}
2350+
2351+
func TestRaceRemovingStaleCVValue(t *testing.T) {
2352+
cacheHitCounter, cacheMissCounter, cacheNumItems, memoryBytesCounted := base.SgwIntStat{}, base.SgwIntStat{}, base.SgwIntStat{}, base.SgwIntStat{}
2353+
backingStoreMap := CreateTestSingleBackingStoreMap(&noopBackingStore{}, testCollectionID)
2354+
cacheOptions := &RevisionCacheOptions{
2355+
MaxItemCount: 10,
2356+
MaxBytes: 0,
2357+
}
2358+
cache := NewLRURevisionCache(cacheOptions, backingStoreMap, &cacheHitCounter, &cacheMissCounter, &cacheNumItems, &memoryBytesCounted)
2359+
2360+
ctx := base.TestCtx(t)
2361+
2362+
var wg sync.WaitGroup
2363+
wg.Add(2)
2364+
2365+
go func() {
2366+
cache.Put(ctx, DocumentRevision{BodyBytes: []byte(`{"some":"data"}`), DocID: "doc1", RevID: "1-abc", CV: &Version{Value: 123, SourceID: "test"}, History: Revisions{"start": 1}}, testCollectionID)
2367+
wg.Done()
2368+
}()
2369+
2370+
go func() {
2371+
cache.RemoveCVOnly(ctx, "doc1", &Version{Value: 123, SourceID: "test"}, testCollectionID)
2372+
wg.Done()
2373+
}()
2374+
2375+
wg.Wait()
2376+
2377+
}
2378+
2379+
func TestEvictionWhenStaleCVRemoved(t *testing.T) {
2380+
cacheHitCounter, cacheMissCounter, cacheNumItems, memoryBytesCounted := base.SgwIntStat{}, base.SgwIntStat{}, base.SgwIntStat{}, base.SgwIntStat{}
2381+
backingStoreMap := CreateTestSingleBackingStoreMap(&noopBackingStore{}, testCollectionID)
2382+
cacheOptions := &RevisionCacheOptions{
2383+
MaxItemCount: 2,
2384+
MaxBytes: 0,
2385+
}
2386+
cache := NewLRURevisionCache(cacheOptions, backingStoreMap, &cacheHitCounter, &cacheMissCounter, &cacheNumItems, &memoryBytesCounted)
2387+
2388+
ctx := base.TestCtx(t)
2389+
2390+
cache.Put(ctx, DocumentRevision{BodyBytes: []byte(`{"some":"data"}`), DocID: "doc1", RevID: "1-abc", CV: &Version{Value: 123, SourceID: "test"}, History: Revisions{"start": 1}}, testCollectionID)
2391+
cache.Put(ctx, DocumentRevision{BodyBytes: []byte(`{"some":"data"}`), DocID: "doc2", RevID: "1-abc", CV: &Version{Value: 1245, SourceID: "test"}, History: Revisions{"start": 1}}, testCollectionID)
2392+
2393+
// remove cv from lookup map for doc1
2394+
cache.RemoveCVOnly(ctx, "doc1", &Version{Value: 123, SourceID: "test"}, testCollectionID)
2395+
2396+
assert.Equal(t, 2, cache.lruList.Len())
2397+
assert.Equal(t, 2, len(cache.cache))
2398+
assert.Equal(t, 1, len(cache.hlvCache))
2399+
// assert that doc2 is only item in hlv lookup
2400+
_, ok := cache.hlvCache[IDandCV{DocID: "doc2", Source: "test", Version: 1245, CollectionID: testCollectionID}]
2401+
assert.True(t, ok)
2402+
2403+
// add new doc revision for doc1 (same cv different revID simulating local wins scenario) to trigger eviction on doc1 first revision
2404+
cache.Put(ctx, DocumentRevision{BodyBytes: []byte(`{"some":"data"}`), DocID: "doc1", RevID: "2-abc", CV: &Version{Value: 123, SourceID: "test"}, History: Revisions{"start": 1}}, testCollectionID)
2405+
2406+
assert.Equal(t, 2, cache.lruList.Len())
2407+
assert.Equal(t, 2, len(cache.cache))
2408+
assert.Equal(t, 2, len(cache.hlvCache))
2409+
2410+
// assert doc1 entry in hlv map has revID 2-abc
2411+
val := cache.hlvCache[IDandCV{DocID: "doc1", Source: "test", Version: 123, CollectionID: testCollectionID}]
2412+
revValue := val.Value.(*revCacheValue)
2413+
assert.Equal(t, "2-abc", revValue.revID)
2414+
// assert doc1 entry in revID map has revID 2-abc
2415+
valRevEntry := cache.cache[IDAndRev{DocID: "doc1", RevID: "2-abc", CollectionID: testCollectionID}]
2416+
revValueRevEntry := valRevEntry.Value.(*revCacheValue)
2417+
assert.Equal(t, "2-abc", revValueRevEntry.revID)
2418+
}

0 commit comments

Comments
 (0)