Skip to content

Commit 6ed9ceb

Browse files
authored
CBG-4734: fix for race when removing a value that is being loaded into rev cache (#7640)
1 parent 4d19118 commit 6ed9ceb

File tree

6 files changed

+106
-4
lines changed

6 files changed

+106
-4
lines changed

db/change_cache.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -503,7 +503,7 @@ func (c *changeCache) DocChanged(event sgbucket.FeedEvent, docType DocumentType)
503503

504504
// Now add the entry for the new doc revision:
505505
if len(rawUserXattr) > 0 {
506-
collection.revisionCache.RemoveWithRev(ctx, docID, syncData.CurrentRev)
506+
collection.revisionCache.RemoveRevOnly(ctx, docID, syncData.CurrentRev)
507507
}
508508

509509
change := &LogEntry{

db/crud.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2496,7 +2496,7 @@ func (db *DatabaseCollectionWithUser) updateAndReturnDoc(ctx context.Context, do
24962496

24972497
// Prior to saving doc, remove the revision in cache
24982498
if createNewRevIDSkipped {
2499-
db.revisionCache.RemoveWithRev(ctx, doc.ID, doc.CurrentRev)
2499+
db.revisionCache.RemoveRevOnly(ctx, doc.ID, doc.CurrentRev)
25002500
}
25012501

25022502
base.DebugfCtx(ctx, base.KeyCRUD, "Saving doc (seq: #%d, id: %v rev: %v)", doc.Sequence, base.UD(doc.ID), doc.CurrentRev)

db/revision_cache_bypass.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,10 @@ func (rc *BypassRevisionCache) RemoveWithRev(ctx context.Context, docID, revID s
128128
// no-op
129129
}
130130

131+
func (rc *BypassRevisionCache) RemoveRevOnly(ctx context.Context, docID, revID string, collectionID uint32) {
132+
// no-op
133+
}
134+
131135
func (rc *BypassRevisionCache) RemoveWithCV(ctx context.Context, docID string, cv *Version, collectionID uint32) {
132136
// no-op
133137
}

db/revision_cache_interface.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ 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(ctx context.Context, docID, revID string, collectionID uint32)
59+
5860
// UpdateDelta stores the given toDelta value in the given rev if cached
5961
UpdateDelta(ctx context.Context, docID, revID string, collectionID uint32, toDelta RevisionDelta)
6062

@@ -172,6 +174,10 @@ func (c *collectionRevisionCache) RemoveWithRev(ctx context.Context, docID, revI
172174
(*c.revCache).RemoveWithRev(ctx, docID, revID, c.collectionID)
173175
}
174176

177+
func (c *collectionRevisionCache) RemoveRevOnly(ctx context.Context, docID, revID string) {
178+
(*c.revCache).RemoveRevOnly(ctx, docID, revID, c.collectionID)
179+
}
180+
175181
// RemoveWithCV is for per collection access to Remove method
176182
func (c *collectionRevisionCache) RemoveWithCV(ctx context.Context, docID string, cv *Version) {
177183
(*c.revCache).RemoveWithCV(ctx, docID, cv, c.collectionID)

db/revision_cache_lru.go

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,10 @@ func (sc *ShardedLRURevisionCache) RemoveWithCV(ctx context.Context, docID strin
9393
sc.getShard(docID).RemoveWithCV(ctx, docID, cv, collectionID)
9494
}
9595

96+
func (sc *ShardedLRURevisionCache) RemoveRevOnly(ctx context.Context, docID, revID string, collectionID uint32) {
97+
sc.getShard(docID).RemoveRevOnly(ctx, docID, revID, collectionID)
98+
}
99+
96100
// An LRU cache of document revision bodies, together with their channel access.
97101
type LRURevisionCache struct {
98102
backingStores map[uint32]RevisionCacheBackingStore
@@ -507,6 +511,12 @@ func (rc *LRURevisionCache) RemoveWithCV(ctx context.Context, docID string, cv *
507511
rc.removeFromCacheByCV(ctx, docID, cv, collectionID)
508512
}
509513

514+
// RemoveRevOnly removes a rev from revision cache lookup map, if present.
515+
func (rc *LRURevisionCache) RemoveRevOnly(ctx context.Context, docID, revID string, collectionID uint32) {
516+
// This will only remove the entry from the rev lookup map, not the lru list
517+
rc.removeFromRevLookup(ctx, docID, revID, collectionID)
518+
}
519+
510520
// removeFromCacheByCV removes an entry from rev cache by CV
511521
func (rc *LRURevisionCache) removeFromCacheByCV(ctx context.Context, docID string, cv *Version, collectionID uint32) {
512522
key := IDandCV{DocID: docID, Source: cv.SourceID, Version: cv.Value, CollectionID: collectionID}
@@ -518,6 +528,7 @@ func (rc *LRURevisionCache) removeFromCacheByCV(ctx context.Context, docID strin
518528
}
519529
// grab the revid key from the value to enable us to remove the reference from the rev lookup map too
520530
elem := element.Value.(*revCacheValue)
531+
521532
legacyKey := IDAndRev{DocID: docID, RevID: elem.revID, CollectionID: collectionID}
522533
rc.lruList.Remove(element)
523534
delete(rc.hlvCache, key)
@@ -538,6 +549,7 @@ func (rc *LRURevisionCache) removeFromCacheByRev(ctx context.Context, docID, rev
538549
}
539550
// grab the cv key from the value to enable us to remove the reference from the rev lookup map too
540551
elem := element.Value.(*revCacheValue)
552+
541553
hlvKey := IDandCV{DocID: docID, Source: elem.cv.SourceID, Version: elem.cv.Value, CollectionID: collectionID}
542554
rc.lruList.Remove(element)
543555
// decrement the overall memory bytes count
@@ -548,6 +560,16 @@ func (rc *LRURevisionCache) removeFromCacheByRev(ctx context.Context, docID, rev
548560
rc.cacheNumItems.Add(-1)
549561
}
550562

563+
// removeFromRevLookup will only remove the entry from the rev lookup map, if present. Underlying element must stay in list for eviction to work.
564+
func (rc *LRURevisionCache) removeFromRevLookup(ctx context.Context, docID, revID string, collectionID uint32) {
565+
key := IDAndRev{DocID: docID, RevID: revID, CollectionID: collectionID}
566+
rc.lock.Lock()
567+
defer rc.lock.Unlock()
568+
// only delete from rev lookup map, if we delete underlying element in list, the now elem in the HLV lookup map
569+
// will never be evicted leading to potential unbounded growth of HLV lookup map
570+
delete(rc.cache, key)
571+
}
572+
551573
// 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.
552574
func (rc *LRURevisionCache) removeValue(value *revCacheValue) {
553575
rc.lock.Lock()
@@ -583,8 +605,17 @@ func (rc *LRURevisionCache) _numberCapacityEviction() (numItemsEvicted int64, nu
583605
}
584606
hlvKey := IDandCV{DocID: value.id, Source: value.cv.SourceID, Version: value.cv.Value, CollectionID: value.collectionID}
585607
revKey := IDAndRev{DocID: value.id, RevID: value.revID, CollectionID: value.collectionID}
586-
delete(rc.cache, revKey)
587608
delete(rc.hlvCache, hlvKey)
609+
if elem := rc.cache[revKey]; elem != nil {
610+
revValue := elem.Value.(*revCacheValue)
611+
// we need to check if the value pointed to by the rev lookup map is the same value we're evicting, this is
612+
// because we can have can currently have two items with the same docID and revID, but different CVs due to
613+
// a new HLV being generated for user xattr updates where we don't generate a new revID.
614+
if revValue.cv.String() == value.cv.String() {
615+
// this rev lookup item matches the value we're evicting, so remove it
616+
delete(rc.cache, revKey)
617+
}
618+
}
588619
numItemsEvicted++
589620
numBytesEvicted += value.getItemBytes()
590621
}
@@ -832,8 +863,17 @@ func (rc *LRURevisionCache) performEviction(ctx context.Context) {
832863
}
833864
revKey := IDAndRev{DocID: value.id, RevID: value.revID, CollectionID: value.collectionID}
834865
hlvKey := IDandCV{DocID: value.id, Source: value.cv.SourceID, Version: value.cv.Value, CollectionID: value.collectionID}
835-
delete(rc.cache, revKey)
836866
delete(rc.hlvCache, hlvKey)
867+
if elem := rc.cache[revKey]; elem != nil {
868+
revValue := elem.Value.(*revCacheValue)
869+
// we need to check if the value pointed to by the rev lookup map is the same value we're evicting, this is
870+
// because we can have can currently have two items with the same docID and revID, but different CVs due to
871+
// a new HLV being generated for user xattr updates where we don't generate a new revID.
872+
if revValue.cv.String() == value.cv.String() {
873+
// this rev lookup item matches the value we're evicting, so remove it
874+
delete(rc.cache, revKey)
875+
}
876+
}
837877
numItemsRemoved++
838878
valueBytes := value.getItemBytes()
839879
numBytesRemoved += valueBytes

db/revision_cache_test.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2239,3 +2239,55 @@ func TestRevCacheOnDemandImportNoCache(t *testing.T) {
22392239
_, exists = collection.revisionCache.Peek(ctx, docID, doc.CurrentRev)
22402240
require.False(t, exists)
22412241
}
2242+
2243+
func TestRemoveFromRevLookup(t *testing.T) {
2244+
cacheHitCounter, cacheMissCounter, cacheNumItems, memoryBytesCounted := base.SgwIntStat{}, base.SgwIntStat{}, base.SgwIntStat{}, base.SgwIntStat{}
2245+
backingStoreMap := CreateTestSingleBackingStoreMap(&noopBackingStore{}, testCollectionID)
2246+
cacheOptions := &RevisionCacheOptions{
2247+
MaxItemCount: 10,
2248+
MaxBytes: 0,
2249+
}
2250+
cache := NewLRURevisionCache(cacheOptions, backingStoreMap, &cacheHitCounter, &cacheMissCounter, &cacheNumItems, &memoryBytesCounted)
2251+
2252+
ctx := base.TestCtx(t)
2253+
2254+
// Fill up the rev cache with the first 10 docs
2255+
for docID := 0; docID < 10; docID++ {
2256+
id := strconv.Itoa(docID)
2257+
vrs := uint64(docID)
2258+
cache.Put(ctx, DocumentRevision{BodyBytes: []byte(`{}`), DocID: id, RevID: "1-abc", CV: &Version{Value: vrs, SourceID: "test"}, History: Revisions{"start": 1}}, testCollectionID)
2259+
}
2260+
assert.Equal(t, int64(10), cacheNumItems.Value())
2261+
assert.Equal(t, int64(20), memoryBytesCounted.Value())
2262+
assert.Equal(t, 10, len(cache.cache))
2263+
assert.Equal(t, 10, len(cache.hlvCache))
2264+
2265+
// simulate a user xattr update:
2266+
// 1. Removes form rev lookup cache
2267+
// 2. Enter new entry with same revID and docID but diff CV
2268+
// 3. Assert that eviction eventually aligns the cache items in each lookup map
2269+
cache.RemoveRevOnly(ctx, "1", "1-abc", testCollectionID)
2270+
assert.Equal(t, int64(10), cacheNumItems.Value())
2271+
assert.Equal(t, int64(20), memoryBytesCounted.Value())
2272+
assert.Equal(t, 10, cache.lruList.Len())
2273+
assert.Equal(t, 9, len(cache.cache))
2274+
assert.Equal(t, 10, len(cache.hlvCache))
2275+
2276+
// add new entry to cache for docID 1 with a different CV but same revID
2277+
cache.Put(ctx, DocumentRevision{BodyBytes: []byte(`{}`), DocID: "1", RevID: "1-abc", CV: &Version{Value: 1234, SourceID: "test"}, History: Revisions{"start": 1}}, testCollectionID)
2278+
assert.Equal(t, int64(10), cacheNumItems.Value())
2279+
assert.Equal(t, int64(20), memoryBytesCounted.Value())
2280+
assert.Equal(t, 10, cache.lruList.Len())
2281+
assert.Equal(t, 9, len(cache.cache)) // we should have 9 items in the rev lookup as we removed one item above
2282+
assert.Equal(t, 10, len(cache.hlvCache))
2283+
2284+
// add new doc to trigger eviction of old doc "1" value in the cache that has entry in hlv cache to an item but the revID lookup
2285+
// for this item will be to a different value (given the simulation of user xattr update above). This means that this
2286+
// will trigger an eviction from CV lookup but no revID lookup thus aligning the lookup maps once again.
2287+
cache.Put(ctx, DocumentRevision{BodyBytes: []byte(`{}`), DocID: "someNewDoc", RevID: "1-abc", CV: &Version{Value: 123456, SourceID: "test"}, History: Revisions{"start": 1}}, testCollectionID)
2288+
assert.Equal(t, int64(10), cacheNumItems.Value())
2289+
assert.Equal(t, int64(20), memoryBytesCounted.Value())
2290+
assert.Equal(t, 10, cache.lruList.Len())
2291+
assert.Equal(t, 10, len(cache.cache)) // we should now have 10 items in rev lookup given the item above aligned the lookups after eviction
2292+
assert.Equal(t, 10, len(cache.hlvCache))
2293+
}

0 commit comments

Comments
 (0)