Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Migrate v2 to using v3 code instead of duplicating it #11

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this is a good idea. First of all, such a dependency feels unnatural and confusing; second - it will bring the v3 package as well as one wanted to include v2. And with vendored deps it will be even more confusing. My vote is to keep them separate

)

// 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
}
Loading