package gcache import ( "time" "github.com/Mzack9999/gcache/internal/list" ) // Discards the least frequently used items first. type LFUCache[K comparable, V any] struct { baseCache[K, V] items map[K]*lfuItem[K, V] freqList *list.List[*freqEntry[K, V]] // list for freqEntry } var _ Cache[string, any] = (*LFUCache[string, any])(nil) type lfuItem[K comparable, V any] struct { clock Clock key K value V freqElement *list.Element[*freqEntry[K, V]] expiration *time.Time } type freqEntry[K comparable, V any] struct { freq uint items map[*lfuItem[K, V]]struct{} } func newLFUCache[K comparable, V any](cb *CacheBuilder[K, V]) *LFUCache[K, V] { c := &LFUCache[K, V]{} buildCache(&c.baseCache, cb) c.init() c.loadGroup.cache = c return c } func (c *LFUCache[K, V]) init() { c.freqList = list.New[*freqEntry[K, V]]() c.items = make(map[K]*lfuItem[K, V], c.size) c.freqList.PushFront(&freqEntry[K, V]{ freq: 0, items: make(map[*lfuItem[K, V]]struct{}), }) } // Set a new key-value pair func (c *LFUCache[K, V]) Set(key K, value V) error { c.mu.Lock() defer c.mu.Unlock() _, err := c.set(key, value) return err } // Set a new key-value pair with an expiration time func (c *LFUCache[K, V]) SetWithExpire(key K, value V, expiration time.Duration) error { c.mu.Lock() defer c.mu.Unlock() item, err := c.set(key, value) if err != nil { return err } t := c.clock.Now().Add(expiration) item.expiration = &t return nil } func (c *LFUCache[K, V]) set(key K, value V) (*lfuItem[K, V], error) { var err error if c.serializeFunc != nil { value, err = c.serializeFunc(key, value) if err != nil { return nil, err } } // Check for existing item item, ok := c.items[key] if ok { item.value = value } else { // Verify size not exceeded if len(c.items) >= c.size { c.evict(1) } item = &lfuItem[K, V]{ clock: c.clock, key: key, value: value, freqElement: nil, } el := c.freqList.Front() fe := el.Value fe.items[item] = struct{}{} item.freqElement = el c.items[key] = item } if c.expiration != nil { t := c.clock.Now().Add(*c.expiration) item.expiration = &t } if c.addedFunc != nil { c.addedFunc(key, value) } return item, nil } // Get a value from cache pool using key if it exists. // If it does not exists key and has LoaderFunc, // generate a value using `LoaderFunc` method returns value. func (c *LFUCache[K, V]) Get(key K) (V, error) { v, err := c.get(key, false) if err == KeyNotFoundError { return c.getWithLoader(key, true) } return v, err } // GetIFPresent gets a value from cache pool using key if it exists. // If it does not exists key, returns KeyNotFoundError. // And send a request which refresh value for specified key if cache object has LoaderFunc. func (c *LFUCache[K, V]) GetIFPresent(key K) (V, error) { v, err := c.get(key, false) if err == KeyNotFoundError { return c.getWithLoader(key, false) } return v, err } func (c *LFUCache[K, V]) get(key K, onLoad bool) (V, error) { v, err := c.getValue(key, onLoad) if err != nil { return c.nilV, err } if c.deserializeFunc != nil { return c.deserializeFunc(key, v) } return v, nil } func (c *LFUCache[K, V]) getValue(key K, onLoad bool) (V, error) { c.mu.Lock() item, ok := c.items[key] if ok { if !item.IsExpired(nil) { c.increment(item) v := item.value c.autoLease(item) c.mu.Unlock() if !onLoad { c.stats.IncrHitCount() } return v, nil } c.removeItem(item) } c.mu.Unlock() if !onLoad { c.stats.IncrMissCount() } return c.nilV, KeyNotFoundError } func (c *LFUCache[K, V]) getWithLoader(key K, isWait bool) (V, error) { if c.loaderExpireFunc == nil { return c.nilV, KeyNotFoundError } value, _, err := c.load(key, func(v V, expiration *time.Duration, e error) (V, error) { if e != nil { return c.nilV, e } c.mu.Lock() defer c.mu.Unlock() item, err := c.set(key, v) if err != nil { return c.nilV, err } if expiration != nil { t := c.clock.Now().Add(*expiration) item.expiration = &t } return v, nil }, isWait) if err != nil { return c.nilV, err } return value, nil } func (c *LFUCache[K, V]) increment(item *lfuItem[K, V]) { currentFreqElement := item.freqElement currentFreqEntry := currentFreqElement.Value nextFreq := currentFreqEntry.freq + 1 delete(currentFreqEntry.items, item) // a boolean whether reuse the empty current entry removable := isRemovableFreqEntry(currentFreqEntry) // insert item into a valid entry nextFreqElement := currentFreqElement.Next() switch { case nextFreqElement == nil || nextFreqElement.Value.freq > nextFreq: if removable { currentFreqEntry.freq = nextFreq nextFreqElement = currentFreqElement } else { nextFreqElement = c.freqList.InsertAfter(&freqEntry[K, V]{ freq: nextFreq, items: make(map[*lfuItem[K, V]]struct{}), }, currentFreqElement) } case nextFreqElement.Value.freq == nextFreq: if removable { c.freqList.Remove(currentFreqElement) } default: panic("unreachable") } nextFreqElement.Value.items[item] = struct{}{} item.freqElement = nextFreqElement } // evict removes the least frequence item from the cache. func (c *LFUCache[K, V]) evict(count int) { entry := c.freqList.Front() for i := 0; i < count; { if entry == nil { return } else { for item := range entry.Value.items { if i >= count { return } c.removeItem(item) i++ } entry = entry.Next() } } } // Has checks if key exists in cache func (c *LFUCache[K, V]) Has(key K) bool { c.mu.RLock() defer c.mu.RUnlock() now := time.Now() return c.has(key, &now) } func (c *LFUCache[K, V]) has(key K, now *time.Time) bool { item, ok := c.items[key] if !ok { return false } return !item.IsExpired(now) } // Remove removes the provided key from the cache. func (c *LFUCache[K, V]) Remove(key K) bool { c.mu.Lock() defer c.mu.Unlock() return c.remove(key) } func (c *LFUCache[K, V]) remove(key K) bool { if item, ok := c.items[key]; ok { c.removeItem(item) return true } return false } // removeElement is used to remove a given list element from the cache func (c *LFUCache[K, V]) removeItem(item *lfuItem[K, V]) { entry := item.freqElement.Value delete(c.items, item.key) delete(entry.items, item) if isRemovableFreqEntry(entry) { c.freqList.Remove(item.freqElement) } if c.evictedFunc != nil { c.evictedFunc(item.key, item.value) } } func (c *LFUCache[K, V]) keys() []K { c.mu.RLock() defer c.mu.RUnlock() keys := make([]K, len(c.items)) var i = 0 for k := range c.items { keys[i] = k i++ } return keys } // GetALL returns all key-value pairs in the cache. func (c *LFUCache[K, V]) GetALL(checkExpired bool) map[K]V { c.mu.RLock() defer c.mu.RUnlock() items := make(map[K]V, len(c.items)) now := time.Now() for k, item := range c.items { if !checkExpired || c.has(k, &now) { items[k] = item.value } } return items } // Keys returns a slice of the keys in the cache. func (c *LFUCache[K, V]) Keys(checkExpired bool) []K { c.mu.RLock() defer c.mu.RUnlock() keys := make([]K, 0, len(c.items)) now := time.Now() for k := range c.items { if !checkExpired || c.has(k, &now) { keys = append(keys, k) } } return keys } // Len returns the number of items in the cache. func (c *LFUCache[K, V]) Len(checkExpired bool) int { c.mu.RLock() defer c.mu.RUnlock() if !checkExpired { return len(c.items) } var length int now := time.Now() for k := range c.items { if c.has(k, &now) { length++ } } return length } // Completely clear the cache func (c *LFUCache[K, V]) Purge() { c.mu.Lock() defer c.mu.Unlock() if c.purgeVisitorFunc != nil { for key, item := range c.items { c.purgeVisitorFunc(key, item.value) } } c.init() } func (c *LFUCache[K, V]) autoLease(item *lfuItem[K, V]) { if item.expiration == nil { return } if c.lease == nil { return } t := item.clock.Now().Add(*c.lease) item.expiration = &t } // IsExpired returns boolean value whether this item is expired or not. func (it *lfuItem[K, V]) IsExpired(now *time.Time) bool { if it.expiration == nil { return false } if now == nil { t := it.clock.Now() now = &t } return it.expiration.Before(*now) } func isRemovableFreqEntry[K comparable, V any](entry *freqEntry[K, V]) bool { return entry.freq != 0 && len(entry.items) == 0 }