Skip to content

Commit

Permalink
migrate v2 to using v3 code instead of duplicating it
Browse files Browse the repository at this point in the history
  • Loading branch information
paskal committed Feb 20, 2024
1 parent 37b55b5 commit 5003f50
Show file tree
Hide file tree
Showing 4 changed files with 36 additions and 206 deletions.
226 changes: 25 additions & 201 deletions v2/cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@
package cache

import (
"container/list"
"fmt"
"sync"
"time"

v3 "github.com/go-pkgz/expirable-cache/v3"
)

// Cache defines cache interface
Expand Down Expand Up @@ -44,225 +44,49 @@ type Stats struct {

// cacheImpl provides Cache interface implementation.
type cacheImpl[K comparable, V any] struct {
ttl time.Duration
maxKeys int
isLRU bool
onEvicted func(key K, value V)

sync.Mutex
stat Stats
items map[K]*list.Element
evictList *list.List
v3.Cache[K, V]
}

// noEvictionTTL - very long ttl to prevent eviction
const noEvictionTTL = time.Hour * 24 * 365 * 10

// NewCache returns a new Cache.
// Default MaxKeys is unlimited (0).
// Default TTL is 10 years, sane value for expirable cache is 5 minutes.
// Default eviction mode is LRC, appropriate option allow to change it to LRU.
func NewCache[K comparable, V any]() Cache[K, V] {
return &cacheImpl[K, V]{
items: map[K]*list.Element{},
evictList: list.New(),
ttl: noEvictionTTL,
maxKeys: 0,
}
return &cacheImpl[K, V]{v3.NewCache[K, V]()}
}

// Set key, ttl of 0 would use cache-wide TTL
func (c *cacheImpl[K, V]) Set(key K, value V, ttl time.Duration) {
c.Lock()
defer c.Unlock()
now := time.Now()
if ttl == 0 {
ttl = c.ttl
}

// Check for existing item
if ent, ok := c.items[key]; ok {
c.evictList.MoveToFront(ent)
ent.Value.(*cacheItem[K, V]).value = value
ent.Value.(*cacheItem[K, V]).expiresAt = now.Add(ttl)
return
}

// Add new item
ent := &cacheItem[K, V]{key: key, value: value, expiresAt: now.Add(ttl)}
entry := c.evictList.PushFront(ent)
c.items[key] = entry
c.stat.Added++

// Remove oldest entry if it is expired, only in case of non-default TTL.
if c.ttl != noEvictionTTL || ttl != noEvictionTTL {
c.removeOldestIfExpired()
}

// Verify size not exceeded
if c.maxKeys > 0 && len(c.items) > c.maxKeys {
c.removeOldest()
}
}

// Get returns the key value if it's not expired
func (c *cacheImpl[K, V]) Get(key K) (V, bool) {
def := *new(V)
c.Lock()
defer c.Unlock()
if ent, ok := c.items[key]; ok {
// Expired item check
if time.Now().After(ent.Value.(*cacheItem[K, V]).expiresAt) {
c.stat.Misses++
return def, false
}
if c.isLRU {
c.evictList.MoveToFront(ent)
}
c.stat.Hits++
return ent.Value.(*cacheItem[K, V]).value, true
value, ok := c.Cache.Get(key)
if !ok {
// preserve v2 behavior of not returning value in case it's expired
// which is not compatible with v3 and simplelru
def := *new(V)
return def, ok
}
c.stat.Misses++
return def, false
return value, ok
}

// Peek returns the key value (or undefined if not found) without updating the "recently used"-ness of the key.
// Works exactly the same as Get in case of LRC mode (default one).
func (c *cacheImpl[K, V]) Peek(key K) (V, bool) {
def := *new(V)
c.Lock()
defer c.Unlock()
if ent, ok := c.items[key]; ok {
// Expired item check
if time.Now().After(ent.Value.(*cacheItem[K, V]).expiresAt) {
c.stat.Misses++
return def, false
}
c.stat.Hits++
return ent.Value.(*cacheItem[K, V]).value, true
value, ok := c.Cache.Peek(key)
if !ok {
// preserve v2 behavior of not returning value in case it's expired
// which is not compatible with v3 and simplelru
def := *new(V)
return def, ok
}
c.stat.Misses++
return def, false
}

// Keys returns a slice of the keys in the cache, from oldest to newest.
func (c *cacheImpl[K, V]) Keys() []K {
c.Lock()
defer c.Unlock()
return c.keys()
}

// Len return count of items in cache, including expired
func (c *cacheImpl[K, V]) Len() int {
c.Lock()
defer c.Unlock()
return c.evictList.Len()
return value, ok
}

// Invalidate key (item) from the cache
func (c *cacheImpl[K, V]) Invalidate(key K) {
c.Lock()
defer c.Unlock()
if ent, ok := c.items[key]; ok {
c.removeElement(ent)
}
}

// InvalidateFn deletes multiple keys if predicate is true
func (c *cacheImpl[K, V]) InvalidateFn(fn func(key K) bool) {
c.Lock()
defer c.Unlock()
for key, ent := range c.items {
if fn(key) {
c.removeElement(ent)
}
}
}

// RemoveOldest remove oldest element in the cache
func (c *cacheImpl[K, V]) RemoveOldest() {
c.Lock()
defer c.Unlock()
c.removeOldest()
}

// DeleteExpired clears cache of expired items
func (c *cacheImpl[K, V]) DeleteExpired() {
c.Lock()
defer c.Unlock()
for _, key := range c.keys() {
if time.Now().After(c.items[key].Value.(*cacheItem[K, V]).expiresAt) {
c.removeElement(c.items[key])
}
}
c.Cache.RemoveOldest()
}

// Purge clears the cache completely.
func (c *cacheImpl[K, V]) Purge() {
c.Lock()
defer c.Unlock()
for k, v := range c.items {
delete(c.items, k)
c.stat.Evicted++
if c.onEvicted != nil {
c.onEvicted(k, v.Value.(*cacheItem[K, V]).value)
}
}
c.evictList.Init()
}

// Stat gets the current stats for cache
func (c *cacheImpl[K, V]) Stat() Stats {
c.Lock()
defer c.Unlock()
return c.stat
}

func (c *cacheImpl[K, V]) String() string {
stats := c.Stat()
size := c.Len()
return fmt.Sprintf("Size: %d, Stats: %+v (%0.1f%%)", size, stats, 100*float64(stats.Hits)/float64(stats.Hits+stats.Misses))
}

// Keys returns a slice of the keys in the cache, from oldest to newest. Has to be called with lock!
func (c *cacheImpl[K, V]) keys() []K {
keys := make([]K, 0, len(c.items))
for ent := c.evictList.Back(); ent != nil; ent = ent.Prev() {
keys = append(keys, ent.Value.(*cacheItem[K, V]).key)
stats := c.Cache.Stat()
return Stats{
Hits: stats.Hits,
Misses: stats.Misses,
Added: stats.Added,
Evicted: stats.Evicted,
}
return keys
}

// removeOldest removes the oldest item from the cache. Has to be called with lock!
func (c *cacheImpl[K, V]) removeOldest() {
ent := c.evictList.Back()
if ent != nil {
c.removeElement(ent)
}
}

// removeOldest removes the oldest item from the cache in case it's already expired. Has to be called with lock!
func (c *cacheImpl[K, V]) removeOldestIfExpired() {
ent := c.evictList.Back()
if ent != nil && time.Now().After(ent.Value.(*cacheItem[K, V]).expiresAt) {
c.removeElement(ent)
}
}

// removeElement is used to remove a given list element from the cache. Has to be called with lock!
func (c *cacheImpl[K, V]) removeElement(e *list.Element) {
c.evictList.Remove(e)
kv := e.Value.(*cacheItem[K, V])
delete(c.items, kv.key)
c.stat.Evicted++
if c.onEvicted != nil {
c.onEvicted(kv.key, kv.value)
}
}

// cacheItem is used to hold a value in the evictList
type cacheItem[K comparable, V any] struct {
expiresAt time.Time
key K
value V
}
7 changes: 6 additions & 1 deletion v2/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,15 @@ module github.com/go-pkgz/expirable-cache/v2

go 1.20

require github.com/stretchr/testify v1.8.4
require (
github.com/go-pkgz/expirable-cache/v3 v3.0.0
github.com/stretchr/testify v1.8.4
)

require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

replace github.com/go-pkgz/expirable-cache/v3 => ../v3
1 change: 1 addition & 0 deletions v2/go.sum
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
Expand Down
8 changes: 4 additions & 4 deletions v2/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,25 +12,25 @@ type options[K comparable, V any] interface {
// WithTTL functional option defines TTL for all cache entries.
// By default, it is set to 10 years, sane option for expirable cache might be 5 minutes.
func (c *cacheImpl[K, V]) WithTTL(ttl time.Duration) Cache[K, V] {
c.ttl = ttl
c.Cache.WithTTL(ttl)
return c
}

// WithMaxKeys functional option defines how many keys to keep.
// By default, it is 0, which means unlimited.
func (c *cacheImpl[K, V]) WithMaxKeys(maxKeys int) Cache[K, V] {
c.maxKeys = maxKeys
c.Cache.WithMaxKeys(maxKeys)
return c
}

// WithLRU sets cache to LRU (Least Recently Used) eviction mode.
func (c *cacheImpl[K, V]) WithLRU() Cache[K, V] {
c.isLRU = true
c.Cache.WithLRU()
return c
}

// WithOnEvicted defined function which would be called automatically for automatically and manually deleted entries
func (c *cacheImpl[K, V]) WithOnEvicted(fn func(key K, value V)) Cache[K, V] {
c.onEvicted = fn
c.Cache.WithOnEvicted(fn)
return c
}

0 comments on commit 5003f50

Please sign in to comment.