Skip to content

Commit 25ab9d0

Browse files
gregns1bbrks
authored andcommitted
CBG-4323: fix for per shard memory based eviction (#7174)
* CBG-4323: fix for per shard memory based eviction * add min size, some test assertions and remove 10% buffer on shards * fix test for CE * update docs * fix failing test on default collection * address comments
1 parent c1d99bd commit 25ab9d0

File tree

5 files changed

+186
-34
lines changed

5 files changed

+186
-34
lines changed

db/revision_cache_lru.go

Lines changed: 42 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ func NewShardedLRURevisionCache(revCacheOptions *RevisionCacheOptions, backingSt
3434
revCacheOptions.MaxItemCount = uint32(perCacheCapacity)
3535
var perCacheMemoryCapacity float32
3636
if revCacheOptions.MaxBytes > 0 {
37-
perCacheMemoryCapacity = 1.1 * float32(revCacheOptions.MaxBytes) / float32(revCacheOptions.ShardCount)
37+
perCacheMemoryCapacity = float32(revCacheOptions.MaxBytes) / float32(revCacheOptions.ShardCount)
3838
revCacheOptions.MaxBytes = int64(perCacheMemoryCapacity)
3939
}
4040

@@ -82,16 +82,17 @@ func (sc *ShardedLRURevisionCache) Remove(docID, revID string, collectionID uint
8282

8383
// An LRU cache of document revision bodies, together with their channel access.
8484
type LRURevisionCache struct {
85-
backingStores map[uint32]RevisionCacheBackingStore
86-
cache map[IDAndRev]*list.Element
87-
lruList *list.List
88-
cacheHits *base.SgwIntStat
89-
cacheMisses *base.SgwIntStat
90-
cacheNumItems *base.SgwIntStat
91-
lock sync.Mutex
92-
capacity uint32
93-
memoryCapacity int64
94-
cacheMemoryBytes *base.SgwIntStat
85+
backingStores map[uint32]RevisionCacheBackingStore
86+
cache map[IDAndRev]*list.Element
87+
lruList *list.List
88+
cacheHits *base.SgwIntStat
89+
cacheMisses *base.SgwIntStat
90+
cacheNumItems *base.SgwIntStat
91+
lock sync.Mutex
92+
capacity uint32 // Max number of items capacity of LRURevisionCache
93+
memoryCapacity int64 // Max memory capacity of LRURevisionCache
94+
currMemoryUsage base.AtomicInt // count of number of bytes used currently in the LRURevisionCache
95+
cacheMemoryBytesStat *base.SgwIntStat // stat for overall revision cache memory usage in bytes. When using sharded cache, will be shared by all shards.
9596
}
9697

9798
// The cache payload data. Stored as the Value of a list Element.
@@ -114,15 +115,15 @@ type revCacheValue struct {
114115
func NewLRURevisionCache(revCacheOptions *RevisionCacheOptions, backingStores map[uint32]RevisionCacheBackingStore, cacheHitStat *base.SgwIntStat, cacheMissStat *base.SgwIntStat, cacheNumItemsStat *base.SgwIntStat, revCacheMemoryStat *base.SgwIntStat) *LRURevisionCache {
115116

116117
return &LRURevisionCache{
117-
cache: map[IDAndRev]*list.Element{},
118-
lruList: list.New(),
119-
capacity: revCacheOptions.MaxItemCount,
120-
backingStores: backingStores,
121-
cacheHits: cacheHitStat,
122-
cacheMisses: cacheMissStat,
123-
cacheNumItems: cacheNumItemsStat,
124-
cacheMemoryBytes: revCacheMemoryStat,
125-
memoryCapacity: revCacheOptions.MaxBytes,
118+
cache: map[IDAndRev]*list.Element{},
119+
lruList: list.New(),
120+
capacity: revCacheOptions.MaxItemCount,
121+
backingStores: backingStores,
122+
cacheHits: cacheHitStat,
123+
cacheMisses: cacheMissStat,
124+
cacheNumItems: cacheNumItemsStat,
125+
cacheMemoryBytesStat: revCacheMemoryStat,
126+
memoryCapacity: revCacheOptions.MaxBytes,
126127
}
127128
}
128129

@@ -151,7 +152,7 @@ func (rc *LRURevisionCache) UpdateDelta(ctx context.Context, docID, revID string
151152
if value != nil {
152153
outGoingBytes := value.updateDelta(toDelta)
153154
if outGoingBytes != 0 {
154-
rc.cacheMemoryBytes.Add(outGoingBytes)
155+
rc.updateRevCacheMemoryUsage(outGoingBytes)
155156
}
156157
// check for memory based eviction
157158
rc.revCacheMemoryBasedEviction()
@@ -169,7 +170,7 @@ func (rc *LRURevisionCache) getFromCache(ctx context.Context, docID, revID strin
169170

170171
if !statEvent && err == nil {
171172
// cache miss so we had to load doc, increment memory count
172-
rc.cacheMemoryBytes.Add(value.getItemBytes())
173+
rc.updateRevCacheMemoryUsage(value.getItemBytes())
173174
// check for memory based eviction
174175
rc.revCacheMemoryBasedEviction()
175176
}
@@ -205,7 +206,7 @@ func (rc *LRURevisionCache) GetActive(ctx context.Context, docID string, collect
205206

206207
if !statEvent && err == nil {
207208
// cache miss so we had to load doc, increment memory count
208-
rc.cacheMemoryBytes.Add(value.getItemBytes())
209+
rc.updateRevCacheMemoryUsage(value.getItemBytes())
209210
// check for rev cache memory based eviction
210211
rc.revCacheMemoryBasedEviction()
211212
}
@@ -234,7 +235,7 @@ func (rc *LRURevisionCache) Put(ctx context.Context, docRev DocumentRevision, co
234235
value := rc.getValue(docRev.DocID, docRev.RevID, collectionID, true)
235236
// increment incoming bytes
236237
docRev.CalculateBytes()
237-
rc.cacheMemoryBytes.Add(docRev.MemoryBytes)
238+
rc.updateRevCacheMemoryUsage(docRev.MemoryBytes)
238239
value.store(docRev)
239240
// check for rev cache memory based eviction
240241
rc.revCacheMemoryBasedEviction()
@@ -250,7 +251,7 @@ func (rc *LRURevisionCache) Upsert(ctx context.Context, docRev DocumentRevision,
250251
if elem := rc.cache[key]; elem != nil {
251252
revItem := elem.Value.(*revCacheValue)
252253
// decrement item bytes by the removed item
253-
rc.cacheMemoryBytes.Add(-revItem.getItemBytes())
254+
rc.updateRevCacheMemoryUsage(-revItem.getItemBytes())
254255
rc.lruList.Remove(elem)
255256
newItem = false
256257
}
@@ -275,13 +276,13 @@ func (rc *LRURevisionCache) Upsert(ctx context.Context, docRev DocumentRevision,
275276

276277
docRev.CalculateBytes()
277278
// add new item bytes to overall count
278-
rc.cacheMemoryBytes.Add(docRev.MemoryBytes)
279+
rc.updateRevCacheMemoryUsage(docRev.MemoryBytes)
279280
value.store(docRev)
280281

281282
// check we aren't over memory capacity, if so perform eviction
282283
numItemsRemoved = 0
283284
if rc.memoryCapacity > 0 {
284-
for rc.cacheMemoryBytes.Value() > rc.memoryCapacity {
285+
for rc.currMemoryUsage.Value() > rc.memoryCapacity {
285286
rc.purgeOldest_()
286287
numItemsRemoved++
287288
}
@@ -332,7 +333,7 @@ func (rc *LRURevisionCache) Remove(docID, revID string, collectionID uint32) {
332333
rc.lruList.Remove(element)
333334
// decrement the overall memory bytes count
334335
revItem := element.Value.(*revCacheValue)
335-
rc.cacheMemoryBytes.Add(-revItem.getItemBytes())
336+
rc.updateRevCacheMemoryUsage(-revItem.getItemBytes())
336337
delete(rc.cache, key)
337338
rc.cacheNumItems.Add(-1)
338339
}
@@ -352,7 +353,7 @@ func (rc *LRURevisionCache) purgeOldest_() {
352353
value := rc.lruList.Remove(rc.lruList.Back()).(*revCacheValue)
353354
delete(rc.cache, value.key)
354355
// decrement memory overall size
355-
rc.cacheMemoryBytes.Add(-value.getItemBytes())
356+
rc.updateRevCacheMemoryUsage(-value.getItemBytes())
356357
}
357358

358359
// Gets the body etc. out of a revCacheValue. If they aren't present already, the loader func
@@ -531,7 +532,7 @@ func (delta *RevisionDelta) CalculateDeltaBytes() {
531532
// revCacheMemoryBasedEviction checks for rev cache eviction, if required calls performEviction which will acquire lock to evict
532533
func (rc *LRURevisionCache) revCacheMemoryBasedEviction() {
533534
// if memory capacity is not set, don't check for eviction this way
534-
if rc.memoryCapacity > 0 && rc.cacheMemoryBytes.Value() > rc.memoryCapacity {
535+
if rc.memoryCapacity > 0 && rc.currMemoryUsage.Value() > rc.memoryCapacity {
535536
rc.performEviction()
536537
}
537538
}
@@ -541,9 +542,19 @@ func (rc *LRURevisionCache) performEviction() {
541542
rc.lock.Lock()
542543
defer rc.lock.Unlock()
543544
var numItemsRemoved int64
544-
for rc.cacheMemoryBytes.Value() > rc.memoryCapacity {
545+
for rc.currMemoryUsage.Value() > rc.memoryCapacity {
545546
rc.purgeOldest_()
546547
numItemsRemoved++
547548
}
548549
rc.cacheNumItems.Add(-numItemsRemoved)
549550
}
551+
552+
// updateRevCacheMemoryUsage atomically increases overall memory usage for cache and the actual rev cache objects usage
553+
func (rc *LRURevisionCache) updateRevCacheMemoryUsage(bytesCount int64) {
554+
// We need to keep track of the current LRURevisionCache memory usage AND the overall usage of the cache. We need
555+
// overall memory usage for the stat added to show rev cache usage plus we need the current rev cache capacity of the
556+
// LRURevisionCache object for sharding the rev cache. This way we can perform eviction on per shard basis much like
557+
// we do with the number of items capacity eviction
558+
rc.currMemoryUsage.Add(bytesCount)
559+
rc.cacheMemoryBytesStat.Add(bytesCount)
560+
}

db/revision_cache_test.go

Lines changed: 105 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -642,7 +642,16 @@ func TestImmediateRevCacheMemoryBasedEviction(t *testing.T) {
642642
assert.Equal(t, int64(0), memoryBytesCounted.Value())
643643
assert.Equal(t, int64(0), cacheNumItems.Value())
644644

645-
docRev, err := cache.Get(ctx, "doc1", "1-abc", testCollectionID, RevCacheOmitDelta)
645+
// assert we can still fetch this upsert doc
646+
docRev, err := cache.Get(ctx, "doc2", "1-abc", testCollectionID, false)
647+
require.NoError(t, err)
648+
assert.Equal(t, "doc2", docRev.DocID)
649+
assert.Equal(t, int64(102), docRev.MemoryBytes)
650+
assert.NotNil(t, docRev.BodyBytes)
651+
assert.Equal(t, int64(0), memoryBytesCounted.Value())
652+
assert.Equal(t, int64(0), cacheNumItems.Value())
653+
654+
docRev, err = cache.Get(ctx, "doc1", "1-abc", testCollectionID, RevCacheOmitDelta)
646655
require.NoError(t, err)
647656
assert.NotNil(t, docRev.BodyBytes)
648657

@@ -657,6 +666,101 @@ func TestImmediateRevCacheMemoryBasedEviction(t *testing.T) {
657666
assert.Equal(t, int64(0), cacheNumItems.Value())
658667
}
659668

669+
// TestShardedMemoryEviction:
670+
// - Test adding a doc to each shard in the test
671+
// - Assert that each shard has individual count for memory usage as expected
672+
// - Add new doc that will take over the shard memory capacity and assert that that eviction takes place and
673+
// all stats are as expected
674+
func TestShardedMemoryEviction(t *testing.T) {
675+
dbcOptions := DatabaseContextOptions{
676+
RevisionCacheOptions: &RevisionCacheOptions{
677+
MaxBytes: 160,
678+
MaxItemCount: 10,
679+
ShardCount: 2,
680+
},
681+
}
682+
db, ctx := SetupTestDBWithOptions(t, dbcOptions)
683+
defer db.Close(ctx)
684+
collection, ctx := GetSingleDatabaseCollectionWithUser(ctx, t, db)
685+
cacheStats := db.DbStats.Cache()
686+
687+
docBody := Body{
688+
"channels": "_default",
689+
}
690+
691+
// add doc that will be added to one shard
692+
size, _ := createDocAndReturnSizeAndRev(t, ctx, "doc1", collection, docBody)
693+
assert.Equal(t, int64(size), cacheStats.RevisionCacheTotalMemory.Value())
694+
// grab this particular shard + assert that the shard memory usage is as expected
695+
shardedCache := db.revisionCache.(*ShardedLRURevisionCache)
696+
doc1Shard := shardedCache.getShard("doc1")
697+
assert.Equal(t, int64(size), doc1Shard.currMemoryUsage.Value())
698+
699+
// add new doc in diff shard + assert that the shard memory usage is as expected
700+
size, _ = createDocAndReturnSizeAndRev(t, ctx, "doc2", collection, docBody)
701+
doc2Shard := shardedCache.getShard("doc2")
702+
assert.Equal(t, int64(size), doc2Shard.currMemoryUsage.Value())
703+
// overall mem usage should be combination oif the two added docs
704+
assert.Equal(t, int64(size*2), cacheStats.RevisionCacheTotalMemory.Value())
705+
706+
// two docs should reside in cache at this time
707+
assert.Equal(t, int64(2), cacheStats.RevisionCacheNumItems.Value())
708+
709+
docBody = Body{
710+
"channels": "_default",
711+
"some": "field",
712+
}
713+
// add new doc to trigger eviction and assert stats are as expected
714+
newDocSize, _ := createDocAndReturnSizeAndRev(t, ctx, "doc3", collection, docBody)
715+
doc3Shard := shardedCache.getShard("doc3")
716+
assert.Equal(t, int64(newDocSize), doc3Shard.currMemoryUsage.Value())
717+
assert.Equal(t, int64(2), cacheStats.RevisionCacheNumItems.Value())
718+
assert.Equal(t, int64(size+newDocSize), cacheStats.RevisionCacheTotalMemory.Value())
719+
}
720+
721+
// TestShardedMemoryEvictionWhenShardEmpty:
722+
// - Test adding a doc to sharded revision cache that will immediately be evicted due to size
723+
// - Assert that stats look as expected
724+
func TestShardedMemoryEvictionWhenShardEmpty(t *testing.T) {
725+
dbcOptions := DatabaseContextOptions{
726+
RevisionCacheOptions: &RevisionCacheOptions{
727+
MaxBytes: 100,
728+
MaxItemCount: 10,
729+
ShardCount: 2,
730+
},
731+
}
732+
db, ctx := SetupTestDBWithOptions(t, dbcOptions)
733+
defer db.Close(ctx)
734+
collection, ctx := GetSingleDatabaseCollectionWithUser(ctx, t, db)
735+
cacheStats := db.DbStats.Cache()
736+
737+
docBody := Body{
738+
"channels": "_default",
739+
}
740+
741+
// add doc that will be added to one shard
742+
rev, _, err := collection.Put(ctx, "doc1", docBody)
743+
require.NoError(t, err)
744+
shardedCache := db.revisionCache.(*ShardedLRURevisionCache)
745+
746+
// assert that doc was not added to cache as it's too large
747+
doc1Shard := shardedCache.getShard("doc1")
748+
assert.Equal(t, int64(0), doc1Shard.currMemoryUsage.Value())
749+
assert.Equal(t, int64(0), cacheStats.RevisionCacheNumItems.Value())
750+
assert.Equal(t, int64(0), cacheStats.RevisionCacheTotalMemory.Value())
751+
752+
// test we can still fetch this doc
753+
docRev, err := collection.GetRev(ctx, "doc1", rev, false, nil)
754+
require.NoError(t, err)
755+
assert.Equal(t, "doc1", docRev.DocID)
756+
assert.NotNil(t, docRev.BodyBytes)
757+
758+
// assert rev cache is still empty
759+
assert.Equal(t, int64(0), doc1Shard.currMemoryUsage.Value())
760+
assert.Equal(t, int64(0), cacheStats.RevisionCacheNumItems.Value())
761+
assert.Equal(t, int64(0), cacheStats.RevisionCacheTotalMemory.Value())
762+
}
763+
660764
func TestImmediateRevCacheItemBasedEviction(t *testing.T) {
661765
cacheHitCounter, cacheMissCounter, getDocumentCounter, getRevisionCounter, cacheNumItems, memoryBytesCounted := base.SgwIntStat{}, base.SgwIntStat{}, base.SgwIntStat{}, base.SgwIntStat{}, base.SgwIntStat{}, base.SgwIntStat{}
662766
backingStoreMap := CreateTestSingleBackingStoreMap(&testBackingStore{nil, &getDocumentCounter, &getRevisionCounter}, testCollectionID)

docs/api/components/schemas.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1307,6 +1307,7 @@ Database:
13071307
max_memory_count_mb:
13081308
description: |-
13091309
The maximum amount of memory the revision cache should take up in MB, setting to 0 will disable any eviction based on memory at rev cache.
1310+
There is a minimum value of 50 (50MB) for this config option.
13101311
When set this memory limit will work in in hand with revision cache size parameter. So you will potentially get eviction at revision cache both based off memory footprint and number of items in the cache.
13111312
**This is an Enterprise Edition feature only**
13121313
type: integer

rest/config_test.go

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3070,6 +3070,7 @@ func TestNotFoundOnInvalidDatabase(t *testing.T) {
30703070
}
30713071

30723072
func TestRevCacheMemoryLimitConfig(t *testing.T) {
3073+
base.SetUpTestLogging(t, base.LevelInfo, base.KeyAll)
30733074
rt := NewRestTester(t, &RestTesterConfig{
30743075
CustomTestBucket: base.GetTestBucket(t),
30753076
PersistentConfig: true,
@@ -3088,7 +3089,7 @@ func TestRevCacheMemoryLimitConfig(t *testing.T) {
30883089
dbConfig.CacheConfig = &CacheConfig{}
30893090
dbConfig.CacheConfig.RevCacheConfig = &RevCacheConfig{
30903091
MaxItemCount: base.Uint32Ptr(100),
3091-
MaxMemoryCountMB: base.Uint32Ptr(4),
3092+
MaxMemoryCountMB: base.Uint32Ptr(51),
30923093
}
30933094
RequireStatus(t, rt.UpsertDbConfig("db1", dbConfig), http.StatusCreated)
30943095

@@ -3102,8 +3103,39 @@ func TestRevCacheMemoryLimitConfig(t *testing.T) {
31023103

31033104
assert.Equal(t, uint32(100), *dbConfig.CacheConfig.RevCacheConfig.MaxItemCount)
31043105
if base.IsEnterpriseEdition() {
3105-
assert.Equal(t, uint32(4), *dbConfig.CacheConfig.RevCacheConfig.MaxMemoryCountMB)
3106+
assert.Equal(t, uint32(51), *dbConfig.CacheConfig.RevCacheConfig.MaxMemoryCountMB)
31063107
} else {
31073108
assert.Nil(t, dbConfig.CacheConfig.RevCacheConfig.MaxMemoryCountMB)
31083109
}
3110+
3111+
dbConfig.CacheConfig = &CacheConfig{}
3112+
dbConfig.CacheConfig.RevCacheConfig = &RevCacheConfig{
3113+
MaxItemCount: base.Uint32Ptr(100),
3114+
MaxMemoryCountMB: base.Uint32Ptr(4),
3115+
}
3116+
resp = rt.UpsertDbConfig("db1", dbConfig)
3117+
if base.IsEnterpriseEdition() {
3118+
assertHTTPErrorReason(t, resp, http.StatusInternalServerError, "Internal error: maximum rev cache memory size cannot be lower than 50 MB")
3119+
} else {
3120+
// CE will roll back to no memory limit as it's an EE ony feature
3121+
RequireStatus(t, resp, http.StatusCreated)
3122+
}
3123+
3124+
// test turing off the memory based rev cache
3125+
dbConfig.CacheConfig = &CacheConfig{}
3126+
dbConfig.CacheConfig.RevCacheConfig = &RevCacheConfig{
3127+
MaxItemCount: base.Uint32Ptr(100),
3128+
MaxMemoryCountMB: base.Uint32Ptr(0),
3129+
}
3130+
RequireStatus(t, rt.UpsertDbConfig("db1", dbConfig), http.StatusCreated)
3131+
3132+
resp = rt.SendAdminRequest(http.MethodGet, "/db1/_config", "")
3133+
RequireStatus(t, resp, http.StatusOK)
3134+
3135+
// empty db config struct
3136+
dbConfig = DbConfig{}
3137+
require.NoError(t, json.Unmarshal(resp.BodyBytes(), &dbConfig))
3138+
assert.NotNil(t, dbConfig.CacheConfig)
3139+
assert.Equal(t, uint32(100), *dbConfig.CacheConfig.RevCacheConfig.MaxItemCount)
3140+
assert.Equal(t, uint32(0), *dbConfig.CacheConfig.RevCacheConfig.MaxMemoryCountMB)
31093141
}

rest/server_context.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1059,6 +1059,10 @@ func dbcOptionsFromConfig(ctx context.Context, sc *ServerContext, config *DbConf
10591059
revCacheOptions.MaxItemCount = *config.CacheConfig.RevCacheConfig.MaxItemCount
10601060
}
10611061
if config.CacheConfig.RevCacheConfig.MaxMemoryCountMB != nil {
1062+
maxMemoryConfigValue := *config.CacheConfig.RevCacheConfig.MaxMemoryCountMB
1063+
if maxMemoryConfigValue != uint32(0) && maxMemoryConfigValue < uint32(50) {
1064+
return db.DatabaseContextOptions{}, fmt.Errorf("maximum rev cache memory size cannot be lower than 50 MB")
1065+
}
10621066
revCacheOptions.MaxBytes = int64(*config.CacheConfig.RevCacheConfig.MaxMemoryCountMB * 1024 * 1024) // Convert MB input to bytes
10631067
}
10641068
if config.CacheConfig.RevCacheConfig.ShardCount != nil {

0 commit comments

Comments
 (0)