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

Changed to use generics #16

Draft
wants to merge 5 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 4 commits
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
113 changes: 76 additions & 37 deletions cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,28 +77,50 @@ type Config struct {
// to the MaxAge
ExpirationInterval time.Duration
// Optional callback invoked when an item is evicted due to the LRU policy
OnEviction func(key, value interface{})
OnEviction func(key interface{}, value interface{})
// Optional callback invoked when an item expired
OnExpiration func(key, value interface{})
OnExpiration func(key interface{}, value interface{})
}

// ConfigGeneric configures the cache.
type ConfigGeneric[T any] struct {
Copy link
Author

Choose a reason for hiding this comment

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

I made a new struct just to make this backwards compatible.
The only thing that isn't backwards compatible, is that now the methods return pointers instead of just interface{}

// Maximum number of items in the cache
Capacity int
// Optional max duration before an item expires. Must be greater than or
// equal to MinAge. If zero, expiration is disabled.
MaxAge time.Duration
// Optional min duration before an item expires. Must be less than or equal
// to MaxAge. When less than MaxAge, uniformly distributed random jitter is
// added to the expiration time. If equal or zero, jitter is disabled.
MinAge time.Duration
// Type of key expiration: Passive or Active
ExpirationType ExpirationType
// For active expiration, how often to iterate over the keyspace. Defaults
// to the MaxAge
ExpirationInterval time.Duration
// Optional callback invoked when an item is evicted due to the LRU policy
OnEviction func(key interface{}, value T)
// Optional callback invoked when an item expired
OnExpiration func(key interface{}, value T)
}

// Entry pointed to by each list.Element
type cacheEntry struct {
type cacheEntry[T any] struct {
key interface{}
value interface{}
value T
timestamp time.Time
}

// Cache implements a thread-safe fixed-capacity LRU cache.
type Cache struct {
type Cache[T any] struct {
// Fields defined by configuration
capacity int
minAge time.Duration
maxAge time.Duration
expirationType ExpirationType
expirationInterval time.Duration
onEviction func(key, value interface{})
onExpiration func(key, value interface{})
onEviction func(key interface{}, value T)
onExpiration func(key interface{}, value T)

// Cache statistics
sets int64
Expand All @@ -117,7 +139,23 @@ type Cache struct {
// must be a positive int, and config.MaxAge a zero or positive duration. A
// duration of zero disables item expiration. Panics given an invalid
// config.Capacity or config.MaxAge.
func New(config Config) *Cache {
func New(config Config) *Cache[interface{}] {
return NewGeneric(ConfigGeneric[interface{}]{
Capacity: config.Capacity,
MaxAge: config.MaxAge,
MinAge: config.MinAge,
ExpirationType: config.ExpirationType,
ExpirationInterval: config.ExpirationInterval,
OnEviction: config.OnEviction,
OnExpiration: config.OnExpiration,
})
}

// NewGeneric constructs an LRU Cache with the given Config object. config.Capacity
// must be a positive int, and config.MaxAge a zero or positive duration. A
// duration of zero disables item expiration. Panics given an invalid
// config.Capacity or config.MaxAge.
func NewGeneric[T any](config ConfigGeneric[T]) *Cache[T] {
if config.Capacity <= 0 {
panic("Must supply a positive config.Capacity")
}
Expand Down Expand Up @@ -146,7 +184,7 @@ func New(config Config) *Cache {

seed := rand.NewSource(time.Now().UnixNano())

cache := &Cache{
cache := &Cache[T]{
capacity: config.Capacity,
maxAge: config.MaxAge,
minAge: minAge,
Expand All @@ -172,7 +210,8 @@ func New(config Config) *Cache {

// Set updates a key:value pair in the cache. Returns true if an eviction
// occurrred, and subsequently invokes the OnEviction callback.
func (cache *Cache) Set(key, value interface{}) bool {
func (cache *Cache[T]) Set(key interface{}, value T) bool {

cache.mutex.Lock()
defer cache.mutex.Unlock()

Expand All @@ -181,13 +220,13 @@ func (cache *Cache) Set(key, value interface{}) bool {

if element, ok := cache.items[key]; ok {
cache.evictionList.MoveToFront(element)
entry := element.Value.(*cacheEntry)
entry := element.Value.(*cacheEntry[T])
entry.value = value
entry.timestamp = timestamp
return false
}

entry := &cacheEntry{key, value, timestamp}
entry := &cacheEntry[T]{key, value, timestamp}
element := cache.evictionList.PushFront(entry)
cache.items[key] = element

Expand All @@ -201,18 +240,18 @@ func (cache *Cache) Set(key, value interface{}) bool {
// Get returns the value stored at `key`. The boolean value reports whether or
// not the value was found. The OnExpiration callback is invoked if the value
// had expired on access
func (cache *Cache) Get(key interface{}) (interface{}, bool) {
func (cache *Cache[T]) Get(key interface{}) (*T, bool) {
cache.mutex.Lock()
defer cache.mutex.Unlock()

cache.gets++

if element, ok := cache.items[key]; ok {
entry := element.Value.(*cacheEntry)
entry := element.Value.(*cacheEntry[T])
if cache.maxAge == 0 || time.Since(entry.timestamp) <= cache.maxAge {
cache.evictionList.MoveToFront(element)
cache.hits++
return entry.value, true
return &entry.value, true
}

// Entry expired
Expand All @@ -230,7 +269,7 @@ func (cache *Cache) Get(key interface{}) (interface{}, bool) {

// Has returns whether or not the `key` is in the cache without updating
// how recently it was accessed or deleting it for having expired.
func (cache *Cache) Has(key interface{}) bool {
func (cache *Cache[T]) Has(key interface{}) bool {
cache.mutex.RLock()
defer cache.mutex.RUnlock()

Expand All @@ -241,20 +280,20 @@ func (cache *Cache) Has(key interface{}) bool {
// Peek returns the value at the specified key and a boolean specifying whether
// or not it was found, without updating how recently it was accessed or
// deleting it for having expired.
func (cache *Cache) Peek(key interface{}) (interface{}, bool) {
func (cache *Cache[T]) Peek(key interface{}) (*T, bool) {
cache.mutex.RLock()
defer cache.mutex.RUnlock()

if element, ok := cache.items[key]; ok {
return element.Value.(*cacheEntry).value, true
return &element.Value.(*cacheEntry[T]).value, true
}

return nil, false
}

// Remove removes the provided key from the cache, returning a bool indicating
// whether or not it existed.
func (cache *Cache) Remove(key interface{}) bool {
func (cache *Cache[T]) Remove(key interface{}) bool {
cache.mutex.Lock()
defer cache.mutex.Unlock()

Expand All @@ -269,23 +308,23 @@ func (cache *Cache) Remove(key interface{}) bool {
// EvictOldest removes the oldest item from the cache, while also invoking any
// eviction callback. A bool is returned indicating whether or not an item was
// removed
func (cache *Cache) EvictOldest() bool {
func (cache *Cache[T]) EvictOldest() bool {
cache.mutex.Lock()
defer cache.mutex.Unlock()

return cache.evictOldest()
}

// Len returns the number of items in the cache.
func (cache *Cache) Len() int {
func (cache *Cache[T]) Len() int {
cache.mutex.RLock()
defer cache.mutex.RUnlock()

return cache.evictionList.Len()
}

// Clear empties the cache.
func (cache *Cache) Clear() {
func (cache *Cache[T]) Clear() {
cache.mutex.Lock()
defer cache.mutex.Unlock()

Expand All @@ -296,7 +335,7 @@ func (cache *Cache) Clear() {
}

// Keys returns all keys in the cache.
func (cache *Cache) Keys() []interface{} {
func (cache *Cache[T]) Keys() []interface{} {
cache.mutex.RLock()
defer cache.mutex.RUnlock()

Expand All @@ -312,15 +351,15 @@ func (cache *Cache) Keys() []interface{} {
}

// OrderedKeys returns all keys in the cache, ordered from oldest to newest.
func (cache *Cache) OrderedKeys() []interface{} {
func (cache *Cache[T]) OrderedKeys() []interface{} {
cache.mutex.RLock()
defer cache.mutex.RUnlock()

keys := make([]interface{}, len(cache.items))
i := 0

for element := cache.evictionList.Back(); element != nil; element = element.Prev() {
keys[i] = element.Value.(*cacheEntry).key
keys[i] = element.Value.(*cacheEntry[T]).key
i++
}

Expand All @@ -330,7 +369,7 @@ func (cache *Cache) OrderedKeys() []interface{} {
// SetMaxAge updates the max age for items in the cache. A duration of zero
// disables expiration. A negative duration, or one that is less than minAge,
// results in an error.
func (cache *Cache) SetMaxAge(maxAge time.Duration) error {
func (cache *Cache[T]) SetMaxAge(maxAge time.Duration) error {
if maxAge < 0 {
return errors.New("Must supply a zero or positive maxAge")
} else if maxAge < cache.minAge {
Expand All @@ -348,7 +387,7 @@ func (cache *Cache) SetMaxAge(maxAge time.Duration) error {
// SetMinAge updates the min age for items in the cache. A duration of zero
// or equal to maxAge disables jitter. A negative duration, or one that is
// greater than maxAge, results in an error.
func (cache *Cache) SetMinAge(minAge time.Duration) error {
func (cache *Cache[T]) SetMinAge(minAge time.Duration) error {
if minAge < 0 {
return errors.New("Must supply a zero or positive minAge")
} else if minAge > cache.maxAge {
Expand All @@ -368,23 +407,23 @@ func (cache *Cache) SetMinAge(minAge time.Duration) error {
}

// OnEviction sets the eviction callback.
func (cache *Cache) OnEviction(callback func(key, value interface{})) {
func (cache *Cache[T]) OnEviction(callback func(key interface{}, value T)) {
cache.mutex.Lock()
defer cache.mutex.Unlock()

cache.onEviction = callback
}

// OnExpiration sets the expiration callback.
func (cache *Cache) OnExpiration(callback func(key, value interface{})) {
func (cache *Cache[T]) OnExpiration(callback func(key interface{}, value T)) {
cache.mutex.Lock()
defer cache.mutex.Unlock()

cache.onExpiration = callback
}

// Stats returns cache stats.
func (cache *Cache) Stats() Stats {
func (cache *Cache[T]) Stats() Stats {
cache.mutex.RLock()
defer cache.mutex.RUnlock()

Expand All @@ -401,7 +440,7 @@ func (cache *Cache) Stats() Stats {

// Resize the cache to hold at most n entries. If n is smaller than the current
// size, entries are evicted to fit the new size. It errors if n <= 0.
func (cache *Cache) Resize(n int) error {
func (cache *Cache[T]) Resize(n int) error {
if n <= 0 {
return errors.New("must supply a positive capacity to Resize")
}
Expand All @@ -421,14 +460,14 @@ func (cache *Cache) Resize(n int) error {
return nil
}

func (cache *Cache) deleteExpired() {
func (cache *Cache[T]) deleteExpired() {
keys := cache.Keys()

for i := range keys {
cache.mutex.Lock()

if element, ok := cache.items[keys[i]]; ok {
entry := element.Value.(*cacheEntry)
entry := element.Value.(*cacheEntry[T])
if cache.maxAge > 0 && time.Since(entry.timestamp) > cache.maxAge {
cache.deleteElement(element)
if cache.onExpiration != nil {
Expand All @@ -441,7 +480,7 @@ func (cache *Cache) deleteExpired() {
}
}

func (cache *Cache) evictOldest() bool {
func (cache *Cache[T]) evictOldest() bool {
element := cache.evictionList.Back()
if element == nil {
return false
Expand All @@ -455,14 +494,14 @@ func (cache *Cache) evictOldest() bool {
return true
}

func (cache *Cache) deleteElement(element *list.Element) *cacheEntry {
func (cache *Cache[T]) deleteElement(element *list.Element) *cacheEntry[T] {
cache.evictionList.Remove(element)
entry := element.Value.(*cacheEntry)
entry := element.Value.(*cacheEntry[T])
delete(cache.items, entry.key)
return entry
}

func (cache *Cache) getTimestamp() time.Time {
func (cache *Cache[T]) getTimestamp() time.Time {
timestamp := time.Now()
if cache.minAge == cache.maxAge {
return timestamp
Expand Down
Loading