go.temporal.io/server/common/cache/lru.go
497 LOC · 315 covered · 182 uncovered · 91 ranges · 4913 concepts · 54 introducers · 1954 tests
File neighbourhood
The centred file is linked to every concept that introduces one of its ranges, every test that runs code from the file, and the gray connector concepts standing between those tests and the file's own introducer concepts. Undirected links join concepts to every file where they introduce source and concepts to the tests they introduce; arrows show specialization between the displayed concepts and bridge only concepts omitted from this view. Concept colors match the source ranges below; connector concepts have no source color and are shown in gray.
Focused file, its introducer and connector concepts, their introduced files, and tests that run code from the file
In the embedded map, ordinary wheel input scrolls the page; use the visible controls to zoom and drag to pan. Open the full-screen map for canvas navigation: wheel pans, Ctrl/Command plus wheel zooms, and arrow keys pan when this region is focused. On touch screens, open the full-screen map to pan or pinch. If JavaScript or WebGL is unavailable, use the related-file, concept, and source links on this page.
Graph controls are ready.
Interactive rendering requires JavaScript and WebGL. Use the related-file, concept, and source links on this page while the interactive map is unavailable.
package cache
import (
"container/list"
"context"
"sync"
"time"
enumspb "go.temporal.io/api/enums/v1"
"go.temporal.io/api/serviceerror"
"go.temporal.io/server/common/clock"
"go.temporal.io/server/common/dynamicconfig"
"go.temporal.io/server/common/goro"
"go.temporal.io/server/common/metrics"
)
var (
// ErrCacheFull is returned if Put fails due to cache being filled with pinned elements
ErrCacheFull = &serviceerror.ResourceExhausted{
Cause: enumspb.RESOURCE_EXHAUSTED_CAUSE_SYSTEM_OVERLOADED,
Scope: enumspb.RESOURCE_EXHAUSTED_SCOPE_SYSTEM,
Message: "cache capacity is fully occupied with pinned elements",
}
// ErrCacheItemTooLarge is returned if Put fails due to item size being larger than max cache capacity
ErrCacheItemTooLarge = serviceerror.NewInternal("cache item size is larger than max cache capacity")
)
const emptyEntrySize = 0
// lru is a concurrent fixed size cache that evicts elements in lru order
type (
lru struct {
mut sync.Mutex
byAccess *list.List
byKey map[any]*list.Element
maxSize int
currSize int
pinnedSize int
onPut func(val any)
onEvict func(val any)
ttl time.Duration
pin bool
timeSource clock.TimeSource
metricsHandler metrics.Handler
backgroundEvict dynamicconfig.TypedPropertyFn[dynamicconfig.CacheBackgroundEvictSettings]
loops goro.Group
}
iteratorImpl struct {
lru *lru
createTime time.Time
nextItem *list.Element
}
entryImpl struct {
key any
createTime time.Time
value any
refCount int
size int
}
)
// Close closes the iterator
it.lru.mut.Unlock()
}
// HasNext return true if there is more items to be returned
return it.nextItem != nil
}
// Next return the next item
if it.nextItem == nil {
panic("LRU cache iterator Next called when there is no next item")
}
it.nextItem = it.nextItem.Next()
// make a copy of the entry so there will be no concurrent access to this entry
entry = &entryImpl{
key: entry.key,
value: entry.value,
size: entry.size,
createTime: entry.createTime,
}
it.prepareNext()
return entry
}
for it.nextItem != nil {
if it.lru.isEntryExpired(entry, it.createTime) {
nextItem := it.nextItem.Next()
it.lru.deleteInternal(it.nextItem)
it.nextItem = nextItem
return
}
}
}
// Iterator returns an iterator to the map. This map
// does not use re-entrant locks, so access or modification
// to the map during iteration can cause a dead lock.
c.mut.Lock()
iterator := &iteratorImpl{
lru: c,
createTime: c.timeSource.Now().UTC(),
nextItem: c.byAccess.Front(),
}
iterator.prepareNext()
return iterator
}
return entry.key
}
return entry.value
}
return entry.size
}
return entry.createTime
}
// New creates a new cache with the given options
return NewWithMetrics(maxSize, opts, metrics.NoopMetricsHandler)
}
// NewWithMetrics creates a new cache that will emit capacity and ttl metrics.
// handler should be tagged with metrics.CacheTypeTag.
func NewWithMetrics(maxSize int, opts *Options, handler metrics.Handler) StoppableCache {
lru.go ×5
if opts == nil {
}
if backgroundEvict == nil {
return dynamicconfig.CacheBackgroundEvictSettings{
Enabled: false,
}
}
}
if timeSource == nil {
}
metrics.CacheTtl.With(handler).Record(opts.TTL)
c := &lru{
byAccess: list.New(),
byKey: make(map[any]*list.Element),
ttl: opts.TTL,
maxSize: maxSize,
currSize: 0,
pin: opts.Pin,
onPut: opts.OnPut,
onEvict: opts.OnEvict,
timeSource: timeSource,
metricsHandler: handler,
backgroundEvict: backgroundEvict,
}
if c.backgroundEvict().Enabled {
}
}
// NewLRU creates a new LRU cache of the given size, setting initial capacity
// to the max size
return New(maxSize, nil)
}
// Get retrieves the value stored under the given key
if c.maxSize == 0 { //
}
defer c.mut.Unlock()
element := c.byKey[key]
if element == nil {
}
if c.isEntryExpired(entry, c.timeSource.Now().UTC()) {
c.deleteInternal(element)
return nil
}
metrics.CacheEntryAgeOnGet.With(c.metricsHandler).Record(c.timeSource.Now().UTC().Sub(entry.createTime))
lru.go ×1
c.updateEntryRefCount(entry)
c.byAccess.MoveToFront(element)
return entry.value
}
// Put puts a new value associated with a given key, returning the existing value (if present)
if c.pin {
panic("Cannot use Put API in Pin mode. Use Delete and PutIfNotExist if necessary")
}
return val
}
// PutIfNotExist puts a value associated with a given key if it does not exist
existing, err := c.putInternal(key, value, false)
if err != nil {
}
return value, err
}
}
// Delete deletes a key, value pair associated with a key
if c.maxSize == 0 {
}
defer c.mut.Unlock()
element := c.byKey[key]
if element != nil {
c.deleteInternal(element)
}
}
// Release decrements the ref count of a pinned element.
if c.maxSize == 0 || !c.pin {
}
defer c.mut.Unlock()
elt, ok := c.byKey[key]
if !ok {
}
entry.refCount--
if entry.refCount == 0 {
metrics.CachePinnedUsage.With(c.metricsHandler).Record(float64(c.pinnedSize))
}
// Entry size might have changed. Recalculate size and evict entries if necessary.
c.currSize = c.calculateNewCacheSize(newEntrySize, entry.Size())
entry.size = newEntrySize
if c.currSize > c.maxSize {
}
}
// Size returns the current size of the lru, useful if cache is not full. This size is calculated by summing
// the size of all entries in the cache. And the entry size is calculated by the size of the value.
// The size of the value is calculated implementing the Sizeable interface. If the value does not implement
// the Sizeable interface, the size is 1.
c.mut.Lock()
defer c.mut.Unlock()
return c.currSize
}
// Put puts a new value associated with a given key, returning the existing value (if present)
// allowUpdate flag is used to control overwrite behavior if the value exists.
if c.maxSize == 0 {
}
if newEntrySize > c.maxSize {
}
defer c.mut.Unlock()
elt := c.byKey[key]
// If the entry exists, check if it has expired or update the value
if elt != nil {
if !c.isEntryExpired(existingEntry, c.timeSource.Now().UTC()) {
existingVal := existingEntry.value
if allowUpdate {
if newCacheSize > c.maxSize {
// calculate again after eviction
newCacheSize = c.calculateNewCacheSize(newEntrySize, existingEntry.Size())
if newCacheSize > c.maxSize {
// This should never happen since allowUpdate is always **true** for non-pinned cache,
// and if all entries are not pinned(ref==0), then the cache should never be full as long as
// new entry's size is less than max size.
// However, to prevent any unexpected behavior, it checks the cache size again.
return nil, ErrCacheFull
}
}
existingEntry.size = newEntrySize
c.currSize = newCacheSize
metrics.CacheUsage.With(c.metricsHandler).Record(float64(c.currSize))
c.updateEntryTTL(existingEntry)
if c.onPut != nil {
}
}
c.byAccess.MoveToFront(elt)
return existingVal, nil
}
// Entry has expired
c.deleteInternal(elt)
}
// check if the new entry can fit in the cache
newCacheSize := c.calculateNewCacheSize(newEntrySize, emptyEntrySize)
if newCacheSize > c.maxSize {
}
key: key,
value: value,
size: newEntrySize,
}
c.updateEntryTTL(entry)
c.updateEntryRefCount(entry)
element := c.byAccess.PushFront(entry)
c.byKey[key] = element
c.currSize = newCacheSize
metrics.CacheUsage.With(c.metricsHandler).Record(float64(c.currSize))
if c.onPut != nil {
}
}
return c.currSize - existingEntrySize + newEntrySize
}
entry := c.byAccess.Remove(element).(*entryImpl)
c.currSize -= entry.Size()
metrics.CacheUsage.With(c.metricsHandler).Record(float64(c.currSize))
metrics.CacheEntryAgeOnEviction.With(c.metricsHandler).Record(c.timeSource.Now().UTC().Sub(entry.createTime))
delete(c.byKey, entry.key)
if c.onEvict != nil {
}
}
// tryEvictUntilCacheSizeUnderLimit tries to evict entries until c.currSize is less than c.maxSize.
c.tryEvictUntilEnoughSpaceWithSkipEntry(0, nil)
}
// tryEvictUntilEnoughSpaceWithSkipEntry try to evict entries until there is enough space for the new entry without
// evicting the existing entry. the existing entry is skipped because it is being updated.
func (c *lru) tryEvictUntilEnoughSpaceWithSkipEntry(newEntrySize int, existingEntry *entryImpl) {
lru.go ×9
element := c.byAccess.Back()
existingEntrySize := 0
if existingEntry != nil {
}
for c.calculateNewCacheSize(newEntrySize, existingEntrySize) > c.maxSize && element != nil {
lru.go ×9
if existingEntry != nil && entry.key == existingEntry.key {
continue
}
}
}
func (c *lru) tryEvictAndGetPreviousElement(entry *entryImpl, element *list.Element) *list.Element {
lru.go ×3
if entry.refCount == 0 {
// currSize will be updated within deleteInternal
c.deleteInternal(element)
return elementPrev
}
// entry.refCount > 0
// skip, entry still being referenced
}
return entry.refCount == 0 && !entry.createTime.IsZero() && currentTime.After(entry.createTime.Add(c.ttl))
}
if c.ttl != 0 {
}
}
if c.pin {
if entry.refCount == 1 {
c.pinnedSize += entry.Size()
metrics.CachePinnedUsage.With(c.metricsHandler).Record(float64(c.pinnedSize))
}
}
}
c.loops.Cancel()
}
ch, t := c.timeSource.NewTimer(c.backgroundEvict().LoopInterval)
for {
select {
case <-ch:
settings := c.backgroundEvict()
if settings.Enabled {
c.bgEvict(settings)
}
t.Reset(settings.LoopInterval)
return ctx.Err()
}
}
}
now := c.timeSource.Now().UTC()
// Limit each iteration to scanning MaxEntryPerCall entries, to avoid holding the cache lock for too long.
evictToMax := func() (again bool) {
c.mut.Lock()
defer c.mut.Unlock()
element := c.byAccess.Back()
if settings.MaxEntryPerCall <= 0 {
return false
}
if element == nil {
return false
}
elementPrev := element.Prev()
entry := element.Value.(*entryImpl) // nolint:revive
if !c.isEntryExpired(entry, now) {
return false
}
c.deleteInternal(element)
element = elementPrev
}
}
}
}