diff --git a/base/stats.go b/base/stats.go index d94968bbdc..464594b3dd 100644 --- a/base/stats.go +++ b/base/stats.go @@ -434,6 +434,8 @@ type CacheStats struct { RevisionCacheHits *SgwIntStat `json:"rev_cache_hits"` // The total number of revision cache misses. RevisionCacheMisses *SgwIntStat `json:"rev_cache_misses"` + // Total memory used by the rev cache + RevisionCacheTotalMemory *SgwIntStat `json:"revision_cache_total_memory"` // The current length of the pending skipped sequence slice. SkippedSeqLen *SgwIntStat `json:"skipped_seq_len"` // The current capacity of the skipped sequence slice @@ -1339,6 +1341,10 @@ func (d *DbStats) initCacheStats() error { if err != nil { return err } + resUtil.RevisionCacheTotalMemory, err = NewIntStat(SubsystemCacheKey, "revision_cache_total_memory", StatUnitNoUnits, RevCacheMemoryDesc, StatAddedVersion3dot2dot1, StatDeprecatedVersionNotDeprecated, StatStabilityCommitted, labelKeys, labelVals, prometheus.GaugeValue, 0) + if err != nil { + return err + } resUtil.SkippedSeqLen, err = NewIntStat(SubsystemCacheKey, "skipped_seq_len", StatUnitNoUnits, SkippedSeqLengthDesc, StatAddedVersion3dot0dot0, StatDeprecatedVersionNotDeprecated, StatStabilityCommitted, labelKeys, labelVals, prometheus.GaugeValue, 0) if err != nil { return err @@ -1388,6 +1394,7 @@ func (d *DbStats) unregisterCacheStats() { prometheus.Unregister(d.CacheStats.RevisionCacheBypass) prometheus.Unregister(d.CacheStats.RevisionCacheHits) prometheus.Unregister(d.CacheStats.RevisionCacheMisses) + prometheus.Unregister(d.CacheStats.RevisionCacheTotalMemory) prometheus.Unregister(d.CacheStats.SkippedSeqLen) prometheus.Unregister(d.CacheStats.ViewQueries) } diff --git a/base/stats_descriptions.go b/base/stats_descriptions.go index a36ebf670f..2ec846dd6e 100644 --- a/base/stats_descriptions.go +++ b/base/stats_descriptions.go @@ -129,6 +129,8 @@ const ( RevCacheMissesDesc = "The total number of revision cache misses. This metric can be used to calculate the ratio of revision cache misses: " + "Rev Cache Miss Ratio = rev_cache_misses / (rev_cache_hits + rev_cache_misses)" + RevCacheMemoryDesc = "The approximation of total memory taken up by rev cache for documents. This is measured by the raw document body, the channels allocated to a document and its revision history." + SkippedSeqLengthDesc = "The current length of the pending skipped sequence slice." SkippedSeqCapDesc = "The current capacity of the skipped sequence slice." diff --git a/db/database_test.go b/db/database_test.go index 329e65e63e..02f0e8e904 100644 --- a/db/database_test.go +++ b/db/database_test.go @@ -402,8 +402,13 @@ func TestGetRemovedAsUser(t *testing.T) { // Manually remove the temporary backup doc from the bucket // Manually flush the rev cache // After expiry from the rev cache and removal of doc backup, try again - cacheHitCounter, cacheMissCounter, cacheNumItems := db.DatabaseContext.DbStats.Cache().RevisionCacheHits, db.DatabaseContext.DbStats.Cache().RevisionCacheMisses, db.DatabaseContext.DbStats.Cache().RevisionCacheNumItems - collection.dbCtx.revisionCache = NewShardedLRURevisionCache(DefaultRevisionCacheShardCount, DefaultRevisionCacheSize, backingStoreMap, cacheHitCounter, cacheMissCounter, cacheNumItems) + cacheHitCounter, cacheMissCounter, cacheNumItems, memoryCacheStat := db.DatabaseContext.DbStats.Cache().RevisionCacheHits, db.DatabaseContext.DbStats.Cache().RevisionCacheMisses, db.DatabaseContext.DbStats.Cache().RevisionCacheNumItems, db.DatabaseContext.DbStats.Cache().RevisionCacheTotalMemory + cacheOptions := &RevisionCacheOptions{ + MaxBytes: 0, + MaxItemCount: DefaultRevisionCacheSize, + ShardCount: DefaultRevisionCacheShardCount, + } + collection.dbCtx.revisionCache = NewShardedLRURevisionCache(cacheOptions, backingStoreMap, cacheHitCounter, cacheMissCounter, cacheNumItems, memoryCacheStat) err = collection.PurgeOldRevisionJSON(ctx, "doc1", rev2id) assert.NoError(t, err, "Purge old revision JSON") @@ -754,8 +759,13 @@ func TestGetRemoved(t *testing.T) { // Manually remove the temporary backup doc from the bucket // Manually flush the rev cache // After expiry from the rev cache and removal of doc backup, try again - cacheHitCounter, cacheMissCounter, cacheNumItems := db.DatabaseContext.DbStats.Cache().RevisionCacheHits, db.DatabaseContext.DbStats.Cache().RevisionCacheMisses, db.DatabaseContext.DbStats.Cache().RevisionCacheNumItems - collection.dbCtx.revisionCache = NewShardedLRURevisionCache(DefaultRevisionCacheShardCount, DefaultRevisionCacheSize, backingStoreMap, cacheHitCounter, cacheMissCounter, cacheNumItems) + cacheHitCounter, cacheMissCounter, cacheNumItems, memoryCacheStat := db.DatabaseContext.DbStats.Cache().RevisionCacheHits, db.DatabaseContext.DbStats.Cache().RevisionCacheMisses, db.DatabaseContext.DbStats.Cache().RevisionCacheNumItems, db.DatabaseContext.DbStats.Cache().RevisionCacheTotalMemory + cacheOptions := &RevisionCacheOptions{ + MaxBytes: 0, + MaxItemCount: DefaultRevisionCacheSize, + ShardCount: DefaultRevisionCacheShardCount, + } + collection.dbCtx.revisionCache = NewShardedLRURevisionCache(cacheOptions, backingStoreMap, cacheHitCounter, cacheMissCounter, cacheNumItems, memoryCacheStat) err = collection.PurgeOldRevisionJSON(ctx, "doc1", rev2id) assert.NoError(t, err, "Purge old revision JSON") @@ -823,8 +833,13 @@ func TestGetRemovedAndDeleted(t *testing.T) { // Manually remove the temporary backup doc from the bucket // Manually flush the rev cache // After expiry from the rev cache and removal of doc backup, try again - cacheHitCounter, cacheMissCounter, cacheNumItems := db.DatabaseContext.DbStats.Cache().RevisionCacheHits, db.DatabaseContext.DbStats.Cache().RevisionCacheMisses, db.DatabaseContext.DbStats.Cache().RevisionCacheNumItems - collection.dbCtx.revisionCache = NewShardedLRURevisionCache(DefaultRevisionCacheShardCount, DefaultRevisionCacheSize, backingStoreMap, cacheHitCounter, cacheMissCounter, cacheNumItems) + cacheHitCounter, cacheMissCounter, cacheNumItems, memoryCacheStats := db.DatabaseContext.DbStats.Cache().RevisionCacheHits, db.DatabaseContext.DbStats.Cache().RevisionCacheMisses, db.DatabaseContext.DbStats.Cache().RevisionCacheNumItems, db.DatabaseContext.DbStats.Cache().RevisionCacheTotalMemory + cacheOptions := &RevisionCacheOptions{ + MaxBytes: 0, + MaxItemCount: DefaultRevisionCacheSize, + ShardCount: DefaultRevisionCacheShardCount, + } + collection.dbCtx.revisionCache = NewShardedLRURevisionCache(cacheOptions, backingStoreMap, cacheHitCounter, cacheMissCounter, cacheNumItems, memoryCacheStats) err = collection.PurgeOldRevisionJSON(ctx, "doc1", rev2id) assert.NoError(t, err, "Purge old revision JSON") diff --git a/db/revision_cache_interface.go b/db/revision_cache_interface.go index 891c8e1b01..9dccd940bd 100644 --- a/db/revision_cache_interface.go +++ b/db/revision_cache_interface.go @@ -68,7 +68,7 @@ func NewRevisionCache(cacheOptions *RevisionCacheOptions, backingStores map[uint cacheOptions = DefaultRevisionCacheOptions() } - if cacheOptions.Size == 0 { + if cacheOptions.MaxItemCount == 0 { bypassStat := cacheStats.RevisionCacheBypass return NewBypassRevisionCache(backingStores, bypassStat) } @@ -76,23 +76,25 @@ func NewRevisionCache(cacheOptions *RevisionCacheOptions, backingStores map[uint cacheHitStat := cacheStats.RevisionCacheHits cacheMissStat := cacheStats.RevisionCacheMisses cacheNumItemsStat := cacheStats.RevisionCacheNumItems + cacheMemoryStat := cacheStats.RevisionCacheTotalMemory if cacheOptions.ShardCount > 1 { - return NewShardedLRURevisionCache(cacheOptions.ShardCount, cacheOptions.Size, backingStores, cacheHitStat, cacheMissStat, cacheNumItemsStat) + return NewShardedLRURevisionCache(cacheOptions, backingStores, cacheHitStat, cacheMissStat, cacheNumItemsStat, cacheMemoryStat) } - return NewLRURevisionCache(cacheOptions.Size, backingStores, cacheHitStat, cacheMissStat, cacheNumItemsStat) + return NewLRURevisionCache(cacheOptions, backingStores, cacheHitStat, cacheMissStat, cacheNumItemsStat, cacheMemoryStat) } type RevisionCacheOptions struct { - Size uint32 - ShardCount uint16 + MaxItemCount uint32 + MaxBytes int64 + ShardCount uint16 } func DefaultRevisionCacheOptions() *RevisionCacheOptions { return &RevisionCacheOptions{ - Size: DefaultRevisionCacheSize, - ShardCount: DefaultRevisionCacheShardCount, + MaxItemCount: DefaultRevisionCacheSize, + ShardCount: DefaultRevisionCacheShardCount, } } @@ -163,7 +165,8 @@ type DocumentRevision struct { Attachments AttachmentsMeta Delta *RevisionDelta Deleted bool - Removed bool // True if the revision is a removal. + Removed bool // True if the revision is a removal. + MemoryBytes int64 // storage of the doc rev bytes measurement, includes size of delta when present too } // MutableBody returns a deep copy of the given document revision as a plain body (without any special properties) @@ -317,10 +320,11 @@ type RevisionDelta struct { ToChannels base.Set // Full list of channels for the to revision RevisionHistory []string // Revision history from parent of ToRevID to source revID, in descending order ToDeleted bool // Flag if ToRevID is a tombstone + totalDeltaBytes int64 // totalDeltaBytes is the total bytes for channels, revisions and body on the delta itself } func newRevCacheDelta(deltaBytes []byte, fromRevID string, toRevision DocumentRevision, deleted bool, toRevAttStorageMeta []AttachmentStorageMeta) RevisionDelta { - return RevisionDelta{ + revDelta := RevisionDelta{ ToRevID: toRevision.RevID, DeltaBytes: deltaBytes, AttachmentStorageMeta: toRevAttStorageMeta, @@ -328,6 +332,8 @@ func newRevCacheDelta(deltaBytes []byte, fromRevID string, toRevision DocumentRe RevisionHistory: toRevision.History.parseAncestorRevisions(fromRevID), ToDeleted: deleted, } + revDelta.CalculateDeltaBytes() + return revDelta } // This is the RevisionCacheLoaderFunc callback for the context's RevisionCache. diff --git a/db/revision_cache_lru.go b/db/revision_cache_lru.go index b32bd28856..b71439f4ee 100644 --- a/db/revision_cache_lru.go +++ b/db/revision_cache_lru.go @@ -26,18 +26,25 @@ type ShardedLRURevisionCache struct { } // Creates a sharded revision cache with the given capacity and an optional loader function. -func NewShardedLRURevisionCache(shardCount uint16, capacity uint32, backingStores map[uint32]RevisionCacheBackingStore, cacheHitStat, cacheMissStat, cacheNumItemsStat *base.SgwIntStat) *ShardedLRURevisionCache { +func NewShardedLRURevisionCache(revCacheOptions *RevisionCacheOptions, backingStores map[uint32]RevisionCacheBackingStore, cacheHitStat, cacheMissStat, cacheNumItemsStat, cacheMemoryStat *base.SgwIntStat) *ShardedLRURevisionCache { - caches := make([]*LRURevisionCache, shardCount) + caches := make([]*LRURevisionCache, revCacheOptions.ShardCount) // Add 10% to per-shared cache capacity to ensure overall capacity is reached under non-ideal shard hashing - perCacheCapacity := 1.1 * float32(capacity) / float32(shardCount) - for i := 0; i < int(shardCount); i++ { - caches[i] = NewLRURevisionCache(uint32(perCacheCapacity+0.5), backingStores, cacheHitStat, cacheMissStat, cacheNumItemsStat) + perCacheCapacity := 1.1 * float32(revCacheOptions.MaxItemCount) / float32(revCacheOptions.ShardCount) + revCacheOptions.MaxItemCount = uint32(perCacheCapacity) + var perCacheMemoryCapacity float32 + if revCacheOptions.MaxBytes > 0 { + perCacheMemoryCapacity = 1.1 * float32(revCacheOptions.MaxBytes) / float32(revCacheOptions.ShardCount) + revCacheOptions.MaxBytes = int64(perCacheMemoryCapacity) + } + + for i := 0; i < int(revCacheOptions.ShardCount); i++ { + caches[i] = NewLRURevisionCache(revCacheOptions, backingStores, cacheHitStat, cacheMissStat, cacheNumItemsStat, cacheMemoryStat) } return &ShardedLRURevisionCache{ caches: caches, - numShards: shardCount, + numShards: revCacheOptions.ShardCount, } } @@ -75,14 +82,16 @@ func (sc *ShardedLRURevisionCache) Remove(docID, revID string, collectionID uint // An LRU cache of document revision bodies, together with their channel access. type LRURevisionCache struct { - backingStores map[uint32]RevisionCacheBackingStore - cache map[IDAndRev]*list.Element - lruList *list.List - cacheHits *base.SgwIntStat - cacheMisses *base.SgwIntStat - cacheNumItems *base.SgwIntStat - lock sync.Mutex - capacity uint32 + backingStores map[uint32]RevisionCacheBackingStore + cache map[IDAndRev]*list.Element + lruList *list.List + cacheHits *base.SgwIntStat + cacheMisses *base.SgwIntStat + cacheNumItems *base.SgwIntStat + lock sync.Mutex + capacity uint32 + memoryCapacity int64 + cacheMemoryBytes *base.SgwIntStat } // The cache payload data. Stored as the Value of a list Element. @@ -98,19 +107,22 @@ type revCacheValue struct { lock sync.RWMutex deleted bool removed bool + itemBytes int64 } // Creates a revision cache with the given capacity and an optional loader function. -func NewLRURevisionCache(capacity uint32, backingStores map[uint32]RevisionCacheBackingStore, cacheHitStat, cacheMissStat, cacheNumItemsStat *base.SgwIntStat) *LRURevisionCache { +func NewLRURevisionCache(revCacheOptions *RevisionCacheOptions, backingStores map[uint32]RevisionCacheBackingStore, cacheHitStat *base.SgwIntStat, cacheMissStat *base.SgwIntStat, cacheNumItemsStat *base.SgwIntStat, revCacheMemoryStat *base.SgwIntStat) *LRURevisionCache { return &LRURevisionCache{ - cache: map[IDAndRev]*list.Element{}, - lruList: list.New(), - capacity: capacity, - backingStores: backingStores, - cacheHits: cacheHitStat, - cacheMisses: cacheMissStat, - cacheNumItems: cacheNumItemsStat, + cache: map[IDAndRev]*list.Element{}, + lruList: list.New(), + capacity: revCacheOptions.MaxItemCount, + backingStores: backingStores, + cacheHits: cacheHitStat, + cacheMisses: cacheMissStat, + cacheNumItems: cacheNumItemsStat, + cacheMemoryBytes: revCacheMemoryStat, + memoryCapacity: revCacheOptions.MaxBytes, } } @@ -137,7 +149,12 @@ func (rc *LRURevisionCache) Peek(ctx context.Context, docID, revID string, colle func (rc *LRURevisionCache) UpdateDelta(ctx context.Context, docID, revID string, collectionID uint32, toDelta RevisionDelta) { value := rc.getValue(docID, revID, collectionID, false) if value != nil { - value.updateDelta(toDelta) + outGoingBytes := value.updateDelta(toDelta) + if outGoingBytes != 0 { + rc.cacheMemoryBytes.Add(outGoingBytes) + } + // check for memory based eviction + rc.revCacheMemoryBasedEviction() } } @@ -150,40 +167,17 @@ func (rc *LRURevisionCache) getFromCache(ctx context.Context, docID, revID strin docRev, statEvent, err := value.load(ctx, rc.backingStores[collectionID], includeDelta) rc.statsRecorderFunc(statEvent) - if err != nil { - rc.removeValue(value) // don't keep failed loads in the cache + if !statEvent && err == nil { + // cache miss so we had to load doc, increment memory count + rc.cacheMemoryBytes.Add(value.getItemBytes()) + // check for memory based eviction + rc.revCacheMemoryBasedEviction() } - return docRev, err -} -// In the event that a revision in invalid it needs to be replaced later and the revision cache value should not be -// used. This function grabs the value directly from the bucket. -func (rc *LRURevisionCache) LoadInvalidRevFromBackingStore(ctx context.Context, key IDAndRev, doc *Document, collectionID uint32, includeDelta bool) (DocumentRevision, error) { - var delta *RevisionDelta - - value := revCacheValue{ - key: key, - } - - // If doc has been passed in use this to grab values. Otherwise run revCacheLoader which will grab the Document - // first - if doc != nil { - value.bodyBytes, value.history, value.channels, value.removed, value.attachments, value.deleted, value.expiry, value.err = revCacheLoaderForDocument(ctx, rc.backingStores[collectionID], doc, key.RevID) - } else { - value.bodyBytes, value.history, value.channels, value.removed, value.attachments, value.deleted, value.expiry, value.err = revCacheLoader(ctx, rc.backingStores[collectionID], key) - } - - if includeDelta { - delta = value.delta + if err != nil { + rc.removeValue(value) // don't keep failed loads in the cache } - - docRev, err := value.asDocumentRevision(delta) - - // Classify operation as a cache miss - rc.statsRecorderFunc(false) - return docRev, err - } // Attempts to retrieve the active revision for a document from the cache. Requires retrieval @@ -209,6 +203,13 @@ func (rc *LRURevisionCache) GetActive(ctx context.Context, docID string, collect docRev, statEvent, err := value.loadForDoc(ctx, rc.backingStores[collectionID], bucketDoc) rc.statsRecorderFunc(statEvent) + if !statEvent && err == nil { + // cache miss so we had to load doc, increment memory count + rc.cacheMemoryBytes.Add(value.getItemBytes()) + // check for rev cache memory based eviction + rc.revCacheMemoryBasedEviction() + } + if err != nil { rc.removeValue(value) // don't keep failed loads in the cache } @@ -231,6 +232,11 @@ func (rc *LRURevisionCache) Put(ctx context.Context, docRev DocumentRevision, co panic("Missing history for RevisionCache.Put") } value := rc.getValue(docRev.DocID, docRev.RevID, collectionID, true) + // increment incoming bytes + docRev.CalculateBytes() + rc.cacheMemoryBytes.Add(docRev.MemoryBytes) + // check for rev cache memory based eviction + rc.revCacheMemoryBasedEviction() value.store(docRev) } @@ -242,6 +248,9 @@ func (rc *LRURevisionCache) Upsert(ctx context.Context, docRev DocumentRevision, newItem := true // If element exists remove from lrulist if elem := rc.cache[key]; elem != nil { + revItem := elem.Value.(*revCacheValue) + // decrement item bytes by the removed item + rc.cacheMemoryBytes.Add(-revItem.getItemBytes()) rc.lruList.Remove(elem) newItem = false } @@ -254,7 +263,7 @@ func (rc *LRURevisionCache) Upsert(ctx context.Context, docRev DocumentRevision, rc.cacheNumItems.Add(1) } - // Purge oldest item if required + // Purge oldest item if over number capacity var numItemsRemoved int for len(rc.cache) > int(rc.capacity) { rc.purgeOldest_() @@ -263,6 +272,18 @@ func (rc *LRURevisionCache) Upsert(ctx context.Context, docRev DocumentRevision, if numItemsRemoved > 0 { rc.cacheNumItems.Add(int64(-numItemsRemoved)) } + + docRev.CalculateBytes() + // add new item bytes to overall count + rc.cacheMemoryBytes.Add(docRev.MemoryBytes) + + // check we aren't over memory capacity, if so perform eviction + if rc.memoryCapacity > 0 { + for rc.cacheMemoryBytes.Value() > rc.memoryCapacity { + rc.purgeOldest_() + } + } + rc.lock.Unlock() value.store(docRev) @@ -283,6 +304,7 @@ func (rc *LRURevisionCache) getValue(docID, revID string, collectionID uint32, c rc.cache[key] = rc.lruList.PushFront(value) rc.cacheNumItems.Add(1) + // evict if over number capacity var numItemsRemoved int for len(rc.cache) > int(rc.capacity) { rc.purgeOldest_() @@ -306,6 +328,9 @@ func (rc *LRURevisionCache) Remove(docID, revID string, collectionID uint32) { return } rc.lruList.Remove(element) + // decrement the overall memory bytes count + revItem := element.Value.(*revCacheValue) + rc.cacheMemoryBytes.Add(-revItem.getItemBytes()) delete(rc.cache, key) rc.cacheNumItems.Add(-1) } @@ -324,6 +349,8 @@ func (rc *LRURevisionCache) removeValue(value *revCacheValue) { func (rc *LRURevisionCache) purgeOldest_() { value := rc.lruList.Remove(rc.lruList.Back()).(*revCacheValue) delete(rc.cache, value.key) + // decrement memory overall size + rc.cacheMemoryBytes.Add(-value.getItemBytes()) } // Gets the body etc. out of a revCacheValue. If they aren't present already, the loader func @@ -361,9 +388,15 @@ func (value *revCacheValue) load(ctx context.Context, backingStore RevisionCache if includeDelta { delta = value.delta } - value.lock.Unlock() docRev, err = value.asDocumentRevision(delta) + // if not cache hit, we loaded from bucket. Calculate doc rev size and assign to rev cache value + if !cacheHit { + docRev.CalculateBytes() + value.itemBytes = docRev.MemoryBytes + } + value.lock.Unlock() + return docRev, cacheHit, err } @@ -408,8 +441,13 @@ func (value *revCacheValue) loadForDoc(ctx context.Context, backingStore Revisio cacheHit = false value.bodyBytes, value.history, value.channels, value.removed, value.attachments, value.deleted, value.expiry, value.err = revCacheLoaderForDocument(ctx, backingStore, doc, value.key.RevID) } - value.lock.Unlock() docRev, err = value.asDocumentRevision(nil) + // if not cache hit, we loaded from bucket. Calculate doc rev size and assign to rev cache value + if !cacheHit { + docRev.CalculateBytes() + value.itemBytes = docRev.MemoryBytes + } + value.lock.Unlock() return docRev, cacheHit, err } @@ -425,12 +463,82 @@ func (value *revCacheValue) store(docRev DocumentRevision) { value.attachments = docRev.Attachments.ShallowCopy() // Don't store attachments the caller might later mutate value.deleted = docRev.Deleted value.err = nil + value.itemBytes = docRev.MemoryBytes } value.lock.Unlock() } -func (value *revCacheValue) updateDelta(toDelta RevisionDelta) { +func (value *revCacheValue) updateDelta(toDelta RevisionDelta) (diffInBytes int64) { value.lock.Lock() + var previousDeltaBytes int64 + if value.delta != nil { + // delta exists, need to pull this to update overall memory size correctly + previousDeltaBytes = value.delta.totalDeltaBytes + } + diffInBytes = toDelta.totalDeltaBytes - previousDeltaBytes value.delta = &toDelta + if diffInBytes != 0 { + value.itemBytes += diffInBytes + } value.lock.Unlock() + return diffInBytes +} + +// getItemBytes acquires read lock and retrieves the rev cache items overall memory footprint +func (value *revCacheValue) getItemBytes() int64 { + value.lock.RLock() + defer value.lock.RUnlock() + return value.itemBytes +} + +// CalculateBytes will calculate the bytes from revisions in the document, body and channels on the document +func (rev *DocumentRevision) CalculateBytes() { + var totalBytes int + for v := range rev.Channels { + bytes := len([]byte(v)) + totalBytes += bytes + } + // calculate history + digests, _ := GetStringArrayProperty(rev.History, RevisionsIds) + historyBytes := 32 * len(digests) + totalBytes += historyBytes + // add body bytes into calculation + totalBytes += len(rev.BodyBytes) + + // convert the int to int64 type and assign to document revision + rev.MemoryBytes = int64(totalBytes) +} + +// CalculateDeltaBytes will calculate bytes from delta channels, delta revisions and delta body +func (delta *RevisionDelta) CalculateDeltaBytes() { + var totalBytes int + for v := range delta.ToChannels { + bytes := len([]byte(v)) + totalBytes += bytes + } + // history calculation + historyBytes := 32 * len(delta.RevisionHistory) + totalBytes += historyBytes + + // account for delta body + totalBytes += len(delta.DeltaBytes) + + delta.totalDeltaBytes = int64(totalBytes) +} + +// revCacheMemoryBasedEviction checks for rev cache eviction, if required calls performEviction which will acquire lock to evict +func (rc *LRURevisionCache) revCacheMemoryBasedEviction() { + // if memory capacity is not set, don't check for eviction this way + if rc.memoryCapacity > 0 && rc.cacheMemoryBytes.Value() > rc.memoryCapacity { + rc.performEviction() + } +} + +// performEviction will evict the oldest items in the cache till we are below the memory threshold +func (rc *LRURevisionCache) performEviction() { + rc.lock.Lock() + defer rc.lock.Unlock() + for rc.cacheMemoryBytes.Value() > rc.memoryCapacity { + rc.purgeOldest_() + } } diff --git a/db/revision_cache_test.go b/db/revision_cache_test.go index 2b41e2d005..0ecc31b85d 100644 --- a/db/revision_cache_test.go +++ b/db/revision_cache_test.go @@ -89,9 +89,13 @@ func CreateTestSingleBackingStoreMap(backingStore RevisionCacheBackingStore, col // Tests the eviction from the LRURevisionCache func TestLRURevisionCacheEviction(t *testing.T) { - cacheHitCounter, cacheMissCounter, cacheNumItems := base.SgwIntStat{}, base.SgwIntStat{}, base.SgwIntStat{} + cacheHitCounter, cacheMissCounter, cacheNumItems, memoryBytesCounted := base.SgwIntStat{}, base.SgwIntStat{}, base.SgwIntStat{}, base.SgwIntStat{} backingStoreMap := CreateTestSingleBackingStoreMap(&noopBackingStore{}, testCollectionID) - cache := NewLRURevisionCache(10, backingStoreMap, &cacheHitCounter, &cacheMissCounter, &cacheNumItems) + cacheOptions := &RevisionCacheOptions{ + MaxItemCount: 10, + MaxBytes: 0, + } + cache := NewLRURevisionCache(cacheOptions, backingStoreMap, &cacheHitCounter, &cacheMissCounter, &cacheNumItems, &memoryBytesCounted) ctx := base.TestCtx(t) @@ -141,11 +145,144 @@ func TestLRURevisionCacheEviction(t *testing.T) { } } +func TestLRURevisionCacheEvictionMemoryBased(t *testing.T) { + dbcOptions := DatabaseContextOptions{ + RevisionCacheOptions: &RevisionCacheOptions{ + MaxBytes: 725, + MaxItemCount: 10, + }, + } + db, ctx := SetupTestDBWithOptions(t, dbcOptions) + defer db.Close(ctx) + collection, ctx := GetSingleDatabaseCollectionWithUser(ctx, t, db) + cacheStats := db.DbStats.Cache() + + smallBody := Body{ + "channels": "_default", // add channel for default sync func in default collection test runs + } + + var currMem, expValue, revZeroSize int64 + for i := 0; i < 10; i++ { + currMem = cacheStats.RevisionCacheTotalMemory.Value() + revSize, _ := createDocAndReturnSizeAndRev(t, ctx, fmt.Sprint(i), collection, smallBody) + if i == 0 { + revZeroSize = int64(revSize) + } + expValue = currMem + int64(revSize) + assert.Equal(t, expValue, cacheStats.RevisionCacheTotalMemory.Value()) + } + + // test eviction by number of items (adding new doc from createDocAndReturnSizeAndRev shouldn't take memory over threshold defined as 730 bytes) + expValue -= revZeroSize // for doc being evicted + docSize, rev := createDocAndReturnSizeAndRev(t, ctx, fmt.Sprint(11), collection, smallBody) + expValue += int64(docSize) + // assert doc 0 been evicted + docRev, ok := db.revisionCache.Peek(ctx, "0", rev, collection.GetCollectionID()) + assert.False(t, ok) + assert.Nil(t, docRev.BodyBytes) + + currMem = cacheStats.RevisionCacheTotalMemory.Value() + // assert total memory is as expected + assert.Equal(t, expValue, currMem) + + // remove doc "1" to give headroom for memory based eviction + db.revisionCache.Remove("1", rev, collection.GetCollectionID()) + docRev, ok = db.revisionCache.Peek(ctx, "1", rev, collection.GetCollectionID()) + assert.False(t, ok) + assert.Nil(t, docRev.BodyBytes) + + // assert current memory from rev cache decreases by the doc size (all docs added thus far are same size) + afterRemoval := currMem - int64(docSize) + assert.Equal(t, afterRemoval, cacheStats.RevisionCacheTotalMemory.Value()) + + // add new doc that will trigger eviction due to taking over memory size + largeBody := Body{ + "type": "test", + "doc": "testDocument", + "foo": "bar", + "lets": "test", + "larger": "document", + "for": "eviction", + "channels": "_default", // add channel for default sync func in default collection test runs + } + _, _, err := collection.Put(ctx, "12", largeBody) + require.NoError(t, err) + + // assert doc "2" has been evicted even though we only have 9 items in cache with capacity of 10, so memory based + // eviction took place + docRev, ok = db.revisionCache.Peek(ctx, "2", rev, collection.GetCollectionID()) + assert.False(t, ok) + assert.Nil(t, docRev.BodyBytes) + + // assert that the overall memory for rev cache is not over maximum + assert.LessOrEqual(t, cacheStats.RevisionCacheTotalMemory.Value(), dbcOptions.RevisionCacheOptions.MaxBytes) +} + +func TestBackingStoreMemoryCalculation(t *testing.T) { + cacheHitCounter, cacheMissCounter, getDocumentCounter, getRevisionCounter, cacheNumItems, memoryBytesCounted := base.SgwIntStat{}, base.SgwIntStat{}, base.SgwIntStat{}, base.SgwIntStat{}, base.SgwIntStat{}, base.SgwIntStat{} + backingStoreMap := CreateTestSingleBackingStoreMap(&testBackingStore{[]string{"doc2"}, &getDocumentCounter, &getRevisionCounter}, testCollectionID) + cacheOptions := &RevisionCacheOptions{ + MaxItemCount: 10, + MaxBytes: 205, + } + cache := NewLRURevisionCache(cacheOptions, backingStoreMap, &cacheHitCounter, &cacheMissCounter, &cacheNumItems, &memoryBytesCounted) + ctx := base.TestCtx(t) + + docRev, err := cache.Get(ctx, "doc1", "1-abc", testCollectionID, RevCacheOmitDelta) + require.NoError(t, err) + assert.Equal(t, "doc1", docRev.DocID) + assert.NotNil(t, docRev.History) + assert.NotNil(t, docRev.Channels) + + currMemStat := memoryBytesCounted.Value() + // assert stats is incremented by appropriate bytes on doc rev + assert.Equal(t, docRev.MemoryBytes, currMemStat) + + // Test get active code pathway of a load from bucket + docRev, err = cache.GetActive(ctx, "doc", testCollectionID) + require.NoError(t, err) + assert.Equal(t, "doc", docRev.DocID) + assert.NotNil(t, docRev.History) + assert.NotNil(t, docRev.Channels) + + newMemStat := currMemStat + docRev.MemoryBytes + // assert stats is incremented by appropriate bytes on doc rev + assert.Equal(t, newMemStat, memoryBytesCounted.Value()) + + // test fail load event doesn't increment memory stat + docRev, err = cache.Get(ctx, "doc2", "1-abc", testCollectionID, RevCacheOmitDelta) + assertHTTPError(t, err, 404) + assert.Nil(t, docRev.BodyBytes) + assert.Equal(t, newMemStat, memoryBytesCounted.Value()) + + // assert length is 2 as expected + assert.Equal(t, 2, cache.lruList.Len()) + + memStatBeforeThirdLoad := memoryBytesCounted.Value() + // test another load from bucket but doing so should trigger memory based eviction + docRev, err = cache.Get(ctx, "doc3", "1-abc", testCollectionID, RevCacheOmitDelta) + require.NoError(t, err) + assert.Equal(t, "doc3", docRev.DocID) + assert.NotNil(t, docRev.History) + assert.NotNil(t, docRev.Channels) + + // assert length is still 2 (eviction took place) + test Peek for first added doc is failure + assert.Equal(t, 2, cache.lruList.Len()) + memStatAfterEviction := (memStatBeforeThirdLoad + docRev.MemoryBytes) - currMemStat + assert.Equal(t, memStatAfterEviction, memoryBytesCounted.Value()) + _, ok := cache.Peek(ctx, "doc1", "1-abc", testCollectionID) + assert.False(t, ok) +} + func TestBackingStore(t *testing.T) { - cacheHitCounter, cacheMissCounter, getDocumentCounter, getRevisionCounter, cacheNumItems := base.SgwIntStat{}, base.SgwIntStat{}, base.SgwIntStat{}, base.SgwIntStat{}, base.SgwIntStat{} + cacheHitCounter, cacheMissCounter, getDocumentCounter, getRevisionCounter, cacheNumItems, memoryBytesCounted := base.SgwIntStat{}, base.SgwIntStat{}, base.SgwIntStat{}, base.SgwIntStat{}, base.SgwIntStat{}, base.SgwIntStat{} backingStoreMap := CreateTestSingleBackingStoreMap(&testBackingStore{[]string{"Peter"}, &getDocumentCounter, &getRevisionCounter}, testCollectionID) - cache := NewLRURevisionCache(10, backingStoreMap, &cacheHitCounter, &cacheMissCounter, &cacheNumItems) + cacheOptions := &RevisionCacheOptions{ + MaxItemCount: 10, + MaxBytes: 0, + } + cache := NewLRURevisionCache(cacheOptions, backingStoreMap, &cacheHitCounter, &cacheMissCounter, &cacheNumItems, &memoryBytesCounted) // Get Rev for the first time - miss cache, but fetch the doc and revision to store docRev, err := cache.Get(base.TestCtx(t), "Jens", "1-abc", testCollectionID, RevCacheOmitDelta) @@ -395,9 +532,13 @@ func TestPutExistingRevRevisionCacheAttachmentProperty(t *testing.T) { // Ensure subsequent updates to delta don't mutate previously retrieved deltas func TestRevisionImmutableDelta(t *testing.T) { - cacheHitCounter, cacheMissCounter, getDocumentCounter, getRevisionCounter, cacheNumItems := base.SgwIntStat{}, base.SgwIntStat{}, base.SgwIntStat{}, base.SgwIntStat{}, base.SgwIntStat{} + cacheHitCounter, cacheMissCounter, getDocumentCounter, getRevisionCounter, cacheNumItems, memoryBytesCounted := base.SgwIntStat{}, base.SgwIntStat{}, base.SgwIntStat{}, base.SgwIntStat{}, base.SgwIntStat{}, base.SgwIntStat{} backingStoreMap := CreateTestSingleBackingStoreMap(&testBackingStore{nil, &getDocumentCounter, &getRevisionCounter}, testCollectionID) - cache := NewLRURevisionCache(10, backingStoreMap, &cacheHitCounter, &cacheMissCounter, &cacheNumItems) + cacheOptions := &RevisionCacheOptions{ + MaxItemCount: 10, + MaxBytes: 0, + } + cache := NewLRURevisionCache(cacheOptions, backingStoreMap, &cacheHitCounter, &cacheMissCounter, &cacheNumItems, &memoryBytesCounted) firstDelta := []byte("delta") secondDelta := []byte("modified delta") @@ -429,11 +570,167 @@ func TestRevisionImmutableDelta(t *testing.T) { } +func TestUpdateDeltaRevCacheMemoryStat(t *testing.T) { + cacheHitCounter, cacheMissCounter, getDocumentCounter, getRevisionCounter, cacheNumItems, memoryBytesCounted := base.SgwIntStat{}, base.SgwIntStat{}, base.SgwIntStat{}, base.SgwIntStat{}, base.SgwIntStat{}, base.SgwIntStat{} + backingStoreMap := CreateTestSingleBackingStoreMap(&testBackingStore{nil, &getDocumentCounter, &getRevisionCounter}, testCollectionID) + cacheOptions := &RevisionCacheOptions{ + MaxItemCount: 10, + MaxBytes: 125, + } + cache := NewLRURevisionCache(cacheOptions, backingStoreMap, &cacheHitCounter, &cacheMissCounter, &cacheNumItems, &memoryBytesCounted) + + firstDelta := []byte("delta") + secondDelta := []byte("modified delta") + thirdDelta := []byte("another delta further modified") + ctx := base.TestCtx(t) + + // Trigger load into cache + docRev, err := cache.Get(ctx, "doc1", "1-abc", testCollectionID, RevCacheIncludeDelta) + assert.NoError(t, err, "Error adding to cache") + + revCacheMem := memoryBytesCounted.Value() + revCacheDelta := newRevCacheDelta(firstDelta, "1-abc", docRev, false, nil) + cache.UpdateDelta(ctx, "doc1", "1-abc", testCollectionID, revCacheDelta) + // assert that rev cache memory increases by expected amount + newMem := revCacheMem + revCacheDelta.totalDeltaBytes + assert.Equal(t, newMem, memoryBytesCounted.Value()) + oldDeltaSize := revCacheDelta.totalDeltaBytes + + newMem = memoryBytesCounted.Value() + revCacheDelta = newRevCacheDelta(secondDelta, "1-abc", docRev, false, nil) + cache.UpdateDelta(ctx, "doc1", "1-abc", testCollectionID, revCacheDelta) + + // assert the overall memory stat is correctly updated (by the diff between the old delta and the new delta) + newMem += revCacheDelta.totalDeltaBytes - oldDeltaSize + assert.Equal(t, newMem, memoryBytesCounted.Value()) + + revCacheDelta = newRevCacheDelta(thirdDelta, "1-abc", docRev, false, nil) + cache.UpdateDelta(ctx, "doc1", "1-abc", testCollectionID, revCacheDelta) + + // assert that eviction took place and as result stat is now 0 (only item in cache was doc1) + assert.Equal(t, int64(0), memoryBytesCounted.Value()) + assert.Equal(t, 0, cache.lruList.Len()) +} + +func TestBasicOperationsOnCacheWithMemoryStat(t *testing.T) { + dbcOptions := DatabaseContextOptions{ + RevisionCacheOptions: &RevisionCacheOptions{ + MaxBytes: 730, + MaxItemCount: 10, + }, + } + db, ctx := SetupTestDBWithOptions(t, dbcOptions) + defer db.Close(ctx) + collection, ctx := GetSingleDatabaseCollectionWithUser(ctx, t, db) + cacheStats := db.DbStats.Cache() + collctionID := collection.GetCollectionID() + + // Test Put on new doc + docSize, revID := createDocAndReturnSizeAndRev(t, ctx, "doc1", collection, Body{"test": "doc"}) + assert.Equal(t, int64(docSize), cacheStats.RevisionCacheTotalMemory.Value()) + + // Test Get with item in the cache + docRev, err := db.revisionCache.Get(ctx, "doc1", revID, collctionID, RevCacheOmitDelta) + require.NoError(t, err) + assert.NotNil(t, docRev.BodyBytes) + assert.Equal(t, int64(docSize), cacheStats.RevisionCacheTotalMemory.Value()) + revIDDoc1 := docRev.RevID + + // Test Get operation with load from bucket, need to first create and remove from rev cache + prevMemStat := cacheStats.RevisionCacheTotalMemory.Value() + revIDDoc2 := createThenRemoveFromRevCache(t, ctx, "doc2", db, collection) + // load from doc from bucket + docRev, err = db.revisionCache.Get(ctx, "doc2", docRev.RevID, collctionID, RevCacheOmitDelta) + require.NoError(t, err) + assert.NotNil(t, docRev.BodyBytes) + assert.Equal(t, "doc2", docRev.DocID) + assert.Greater(t, cacheStats.RevisionCacheTotalMemory.Value(), prevMemStat) + + // Test Get active with item resident in cache + prevMemStat = cacheStats.RevisionCacheTotalMemory.Value() + docRev, err = db.revisionCache.GetActive(ctx, "doc2", collctionID) + require.NoError(t, err) + assert.Equal(t, "doc2", docRev.DocID) + assert.Equal(t, prevMemStat, cacheStats.RevisionCacheTotalMemory.Value()) + + // Test Get active with item to be loaded from bucket, need to first create and remove from rev cache + prevMemStat = cacheStats.RevisionCacheTotalMemory.Value() + revIDDoc3 := createThenRemoveFromRevCache(t, ctx, "doc3", db, collection) + docRev, err = db.revisionCache.GetActive(ctx, "doc3", collctionID) + require.NoError(t, err) + assert.Equal(t, "doc3", docRev.DocID) + assert.Greater(t, cacheStats.RevisionCacheTotalMemory.Value(), prevMemStat) + + // Test Peek at item not in cache, assert stats unchanged + prevMemStat = cacheStats.RevisionCacheTotalMemory.Value() + docRev, ok := db.revisionCache.Peek(ctx, "doc4", "1-abc", collctionID) + require.False(t, ok) + assert.Nil(t, docRev.BodyBytes) + assert.Equal(t, prevMemStat, cacheStats.RevisionCacheTotalMemory.Value()) + + // Test Peek in cache, assert stat unchanged + docRev, ok = db.revisionCache.Peek(ctx, "doc3", revIDDoc3, collctionID) + require.True(t, ok) + assert.Equal(t, "doc3", docRev.DocID) + assert.Equal(t, prevMemStat, cacheStats.RevisionCacheTotalMemory.Value()) + + // Test Upsert with item in cache + assert stat is expected + docRev.CalculateBytes() + doc3Size := docRev.MemoryBytes + expMem := cacheStats.RevisionCacheTotalMemory.Value() - doc3Size + newDocRev := DocumentRevision{ + DocID: "doc3", + RevID: revIDDoc3, + BodyBytes: []byte(`"some": "body"`), + } + expMem = expMem + 14 // size for above doc rev + db.revisionCache.Upsert(ctx, newDocRev, collctionID) + assert.Equal(t, expMem, cacheStats.RevisionCacheTotalMemory.Value()) + + // Test Upsert with item not in cache, assert stat is as expected + newDocRev = DocumentRevision{ + DocID: "doc5", + RevID: "1-abc", + BodyBytes: []byte(`"some": "body"`), + } + expMem = cacheStats.RevisionCacheTotalMemory.Value() + 14 // size for above doc rev + db.revisionCache.Upsert(ctx, newDocRev, collctionID) + assert.Equal(t, expMem, cacheStats.RevisionCacheTotalMemory.Value()) + + // Test Remove with something in cache, assert stat decrements by expected value + db.revisionCache.Remove("doc5", "1-abc", collctionID) + expMem -= 14 + assert.Equal(t, expMem, cacheStats.RevisionCacheTotalMemory.Value()) + + // Test Remove with item not in cache, assert stat is unchanged + prevMemStat = cacheStats.RevisionCacheTotalMemory.Value() + db.revisionCache.Remove("doc6", "1-abc", collctionID) + assert.Equal(t, prevMemStat, cacheStats.RevisionCacheTotalMemory.Value()) + + // Test Update Delta, assert stat increases as expected + revDelta := newRevCacheDelta([]byte(`"rev":"delta"`), "1-abc", newDocRev, false, nil) + expMem = prevMemStat + revDelta.totalDeltaBytes + db.revisionCache.UpdateDelta(ctx, "doc3", revIDDoc3, collctionID, revDelta) + assert.Equal(t, expMem, cacheStats.RevisionCacheTotalMemory.Value()) + + // Empty cache and see memory stat is 0 + db.revisionCache.Remove("doc3", revIDDoc3, collctionID) + db.revisionCache.Remove("doc2", revIDDoc2, collctionID) + db.revisionCache.Remove("doc1", revIDDoc1, collctionID) + + // TODO: pending CBG-4135 assert rev cache had 0 items in it + assert.Equal(t, int64(0), cacheStats.RevisionCacheTotalMemory.Value()) +} + // Ensure subsequent updates to delta don't mutate previously retrieved deltas func TestSingleLoad(t *testing.T) { - cacheHitCounter, cacheMissCounter, getDocumentCounter, getRevisionCounter, cacheNumItems := base.SgwIntStat{}, base.SgwIntStat{}, base.SgwIntStat{}, base.SgwIntStat{}, base.SgwIntStat{} + cacheHitCounter, cacheMissCounter, getDocumentCounter, getRevisionCounter, cacheNumItems, memoryBytesCounted := base.SgwIntStat{}, base.SgwIntStat{}, base.SgwIntStat{}, base.SgwIntStat{}, base.SgwIntStat{}, base.SgwIntStat{} backingStoreMap := CreateTestSingleBackingStoreMap(&testBackingStore{nil, &getDocumentCounter, &getRevisionCounter}, testCollectionID) - cache := NewLRURevisionCache(10, backingStoreMap, &cacheHitCounter, &cacheMissCounter, &cacheNumItems) + cacheOptions := &RevisionCacheOptions{ + MaxItemCount: 10, + MaxBytes: 0, + } + cache := NewLRURevisionCache(cacheOptions, backingStoreMap, &cacheHitCounter, &cacheMissCounter, &cacheNumItems, &memoryBytesCounted) cache.Put(base.TestCtx(t), DocumentRevision{BodyBytes: []byte(`{"test":"1234"}`), DocID: "doc123", RevID: "1-abc", History: Revisions{"start": 1}}, testCollectionID) _, err := cache.Get(base.TestCtx(t), "doc123", "1-abc", testCollectionID, false) @@ -442,9 +739,13 @@ func TestSingleLoad(t *testing.T) { // Ensure subsequent updates to delta don't mutate previously retrieved deltas func TestConcurrentLoad(t *testing.T) { - cacheHitCounter, cacheMissCounter, getDocumentCounter, getRevisionCounter, cacheNumItems := base.SgwIntStat{}, base.SgwIntStat{}, base.SgwIntStat{}, base.SgwIntStat{}, base.SgwIntStat{} + cacheHitCounter, cacheMissCounter, getDocumentCounter, getRevisionCounter, cacheNumItems, memoryBytesCounted := base.SgwIntStat{}, base.SgwIntStat{}, base.SgwIntStat{}, base.SgwIntStat{}, base.SgwIntStat{}, base.SgwIntStat{} backingStoreMap := CreateTestSingleBackingStoreMap(&testBackingStore{nil, &getDocumentCounter, &getRevisionCounter}, testCollectionID) - cache := NewLRURevisionCache(10, backingStoreMap, &cacheHitCounter, &cacheMissCounter, &cacheNumItems) + cacheOptions := &RevisionCacheOptions{ + MaxItemCount: 10, + MaxBytes: 0, + } + cache := NewLRURevisionCache(cacheOptions, backingStoreMap, &cacheHitCounter, &cacheMissCounter, &cacheNumItems, &memoryBytesCounted) cache.Put(base.TestCtx(t), DocumentRevision{BodyBytes: []byte(`{"test":"1234"}`), DocID: "doc1", RevID: "1-abc", History: Revisions{"start": 1}}, testCollectionID) @@ -562,7 +863,7 @@ func TestRevCacheHitMultiCollectionLoadFromBucket(t *testing.T) { // create database context with rev cache size 1 dbOptions := DatabaseContextOptions{ RevisionCacheOptions: &RevisionCacheOptions{ - Size: 1, + MaxItemCount: 1, }, } dbOptions.Scopes = GetScopesOptions(t, tb, 2) @@ -609,9 +910,13 @@ func TestRevCacheHitMultiCollectionLoadFromBucket(t *testing.T) { } func TestRevCacheCapacityStat(t *testing.T) { - cacheHitCounter, cacheMissCounter, getDocumentCounter, getRevisionCounter, cacheNumItems := base.SgwIntStat{}, base.SgwIntStat{}, base.SgwIntStat{}, base.SgwIntStat{}, base.SgwIntStat{} + cacheHitCounter, cacheMissCounter, getDocumentCounter, getRevisionCounter, cacheNumItems, cacheMemoryStat := base.SgwIntStat{}, base.SgwIntStat{}, base.SgwIntStat{}, base.SgwIntStat{}, base.SgwIntStat{}, base.SgwIntStat{} backingStoreMap := CreateTestSingleBackingStoreMap(&testBackingStore{[]string{"badDoc"}, &getDocumentCounter, &getRevisionCounter}, testCollectionID) - cache := NewLRURevisionCache(4, backingStoreMap, &cacheHitCounter, &cacheMissCounter, &cacheNumItems) + cacheOptions := &RevisionCacheOptions{ + MaxItemCount: 4, + MaxBytes: 0, + } + cache := NewLRURevisionCache(cacheOptions, backingStoreMap, &cacheHitCounter, &cacheMissCounter, &cacheNumItems, &cacheMemoryStat) ctx := base.TestCtx(t) assert.Equal(t, int64(0), cacheNumItems.Value()) @@ -703,9 +1008,13 @@ func TestRevCacheCapacityStat(t *testing.T) { func BenchmarkRevisionCacheRead(b *testing.B) { base.SetUpBenchmarkLogging(b, base.LevelDebug, base.KeyAll) - cacheHitCounter, cacheMissCounter, getDocumentCounter, getRevisionCounter, cacheNumItems := base.SgwIntStat{}, base.SgwIntStat{}, base.SgwIntStat{}, base.SgwIntStat{}, base.SgwIntStat{} + cacheHitCounter, cacheMissCounter, getDocumentCounter, getRevisionCounter, cacheNumItems, memoryBytesCounted := base.SgwIntStat{}, base.SgwIntStat{}, base.SgwIntStat{}, base.SgwIntStat{}, base.SgwIntStat{}, base.SgwIntStat{} backingStoreMap := CreateTestSingleBackingStoreMap(&testBackingStore{nil, &getDocumentCounter, &getRevisionCounter}, testCollectionID) - cache := NewLRURevisionCache(5000, backingStoreMap, &cacheHitCounter, &cacheMissCounter, &cacheNumItems) + cacheOptions := &RevisionCacheOptions{ + MaxItemCount: DefaultRevisionCacheSize, + MaxBytes: 0, + } + cache := NewLRURevisionCache(cacheOptions, backingStoreMap, &cacheHitCounter, &cacheMissCounter, &cacheNumItems, &memoryBytesCounted) ctx := base.TestCtx(b) @@ -723,3 +1032,36 @@ func BenchmarkRevisionCacheRead(b *testing.B) { } }) } + +// createThenRemoveFromRevCache will create a doc and then immediately remove it from the rev cache +func createThenRemoveFromRevCache(t *testing.T, ctx context.Context, docID string, db *Database, collection *DatabaseCollectionWithUser) string { + revIDDoc, _, err := collection.Put(ctx, docID, Body{"test": "doc"}) + require.NoError(t, err) + + db.revisionCache.Remove(docID, revIDDoc, collection.GetCollectionID()) + + return revIDDoc +} + +// createDocAndReturnSizeAndRev creates a rev and measures its size based on rev cache measurements +func createDocAndReturnSizeAndRev(t *testing.T, ctx context.Context, docID string, collection *DatabaseCollectionWithUser, body Body) (int, string) { + + rev, doc, err := collection.Put(ctx, docID, body) + require.NoError(t, err) + + var expectedSize int + his, err := doc.SyncData.History.getHistory(rev) + require.NoError(t, err) + + historyBytes := 32 * len(his) + expectedSize += historyBytes + expectedSize += len(doc._rawBody) + + // channels + chanArray := doc.Channels.KeySet() + for _, v := range chanArray { + expectedSize += len([]byte(v)) + } + + return expectedSize, rev +} diff --git a/db/revtree.go b/db/revtree.go index 08c2dc11bf..b4f14f3f60 100644 --- a/db/revtree.go +++ b/db/revtree.go @@ -724,12 +724,15 @@ func (tree RevTree) RenderGraphvizDot() string { func (tree RevTree) getHistory(revid string) ([]string, error) { maxHistory := len(tree) + var totalBytesForHistory int history := make([]string, 0, 5) for revid != "" { info, err := tree.getInfo(revid) if err != nil { break } + revBytes := len([]byte(revid)) + totalBytesForHistory += revBytes history = append(history, revid) if len(history) > maxHistory { return history, fmt.Errorf("getHistory found cycle in revision tree, history calculated as: %v", history) diff --git a/rest/api_benchmark_test.go b/rest/api_benchmark_test.go index f8e61afa73..db49501e39 100644 --- a/rest/api_benchmark_test.go +++ b/rest/api_benchmark_test.go @@ -127,7 +127,7 @@ func BenchmarkReadOps_GetRevCacheMisses(b *testing.B) { // Get database handle rtDatabase := rt.GetDatabase() - revCacheSize := rtDatabase.Options.RevisionCacheOptions.Size + revCacheSize := rtDatabase.Options.RevisionCacheOptions.MaxItemCount doc1k_putDoc := fmt.Sprintf(doc_1k_format, "") numDocs := int(revCacheSize + 1) diff --git a/rest/server_context.go b/rest/server_context.go index d7f2d1fe8a..261269449c 100644 --- a/rest/server_context.go +++ b/rest/server_context.go @@ -1165,7 +1165,7 @@ func dbcOptionsFromConfig(ctx context.Context, sc *ServerContext, config *DbConf if config.CacheConfig.RevCacheConfig != nil { if config.CacheConfig.RevCacheConfig.Size != nil { - revCacheOptions.Size = *config.CacheConfig.RevCacheConfig.Size + revCacheOptions.MaxItemCount = *config.CacheConfig.RevCacheConfig.Size } if config.CacheConfig.RevCacheConfig.ShardCount != nil { revCacheOptions.ShardCount = *config.CacheConfig.RevCacheConfig.ShardCount