diff --git a/README.md b/README.md index 9b6a40c..e3586b7 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ `imcache` is a generic in-memory cache Go library. -It supports expiration, sliding expiration, max entries limit, eviction callbacks and sharding. It's safe for concurrent use by multiple goroutines. +It supports absolute expiration, sliding expiration, max entries limit, eviction callbacks and sharding. It's safe for concurrent use by multiple goroutines. ```Go import "github.com/erni27/imcache" @@ -43,15 +43,26 @@ func main() { ### Expiration -`imcache` supports no expiration, absolute expiration and sliding expiration. No expiration simply means that the entry will never expire, absolute expiration means that the entry will expire after a certain time and sliding expiration means that the entry will expire after a certain time if it hasn't been accessed. +`imcache` supports no expiration, absolute expiration and sliding expiration. + +* No expiration means that the entry will never expire. +* Absolute expiration means that the entry will expire after a certain time. +* Sliding expiration means that the entry will expire after a certain time if it hasn't been accessed. The expiration time is reset every time the entry is accessed. ```go // Set a new value with no expiration time. +// The entry will never expire. c.Set(1, "one", imcache.WithNoExpiration()) // Set a new value with a sliding expiration time. +// If the entry hasn't been accessed in the last 1 second, it will be evicted, +// otherwise the expiration time will be extended by 1 second from the last access time. c.Set(2, "two", imcache.WithSlidingExpiration(time.Second)) -// Set a new value with an absolute expiration time. +// Set a new value with an absolute expiration time set to 1 second from now. +// The entry will expire after 1 second. c.Set(3, "three", imcache.WithExpiration(time.Second)) +// Set a new value with an absolute expiration time set to a specific date. +// The entry will expire at the given date. +c.Set(4, "four", imcache.WithExpirationDate(time.Now().Add(time.Second))) ``` If you want to use default expiration time for the given cache instance, you can use the `WithDefaultExpiration` `Expiration` option. By default the default expiration time is set to no expiration. You can set the default expiration time when creating a new `Cache` or a `Sharded` instance. More on sharding can be found in the [Sharding](#sharding) section. @@ -141,7 +152,7 @@ All previous examples apply to `Sharded` type as well. Note that `Option`(s) are ## Performance -`imcache` is designed to be simple and efficient. It uses a vanilla Go map to store entries and a simple and efficient implementation of double linked list to maintain LRU order. The latter is used to evict the least recently used entry when the max entries limit is reached hence if the max entries limit is not set, `imcache` doesn't maintain any additional data structures. +`imcache` is designed to be simple and efficient. It uses a vanilla Go map to store entries and a simple and an efficient implementation of double linked list to maintain LRU order. The latter is used to evict the least recently used entry when the max entries limit is reached hence if the max entries limit is not set, `imcache` doesn't maintain any additional data structures. `imcache` was compared to the vanilla Go map with simple locking mechanism. The benchmarks were run on an Apple M1 Pro 8-core CPU with 32 GB of RAM running macOS Ventura 13.1 using Go 1.20.3. diff --git a/cache.go b/cache.go index 115683c..6a71d8e 100644 --- a/cache.go +++ b/cache.go @@ -1,5 +1,5 @@ // Package imcache provides a generic in-memory cache. -// It supports expiration, sliding expiration, max entries limit, +// It supports absolute expiration, sliding expiration, max entries limit, // eviction callbacks and sharding. // It's safe for concurrent use by multiple goroutines. // @@ -80,27 +80,27 @@ func (c *Cache[K, V]) Get(key K) (V, bool) { now := time.Now() var empty V c.mu.Lock() - entry, ok := c.m[key] + current, ok := c.m[key] if !ok { c.mu.Unlock() return empty, false } - c.queue.Remove(entry.node) - if entry.HasExpired(now) { + c.queue.Remove(current.node) + if current.HasExpired(now) { delete(c.m, key) c.mu.Unlock() if c.onEviction != nil { - c.onEviction(key, entry.val, EvictionReasonExpired) + c.onEviction(key, current.val, EvictionReasonExpired) } return empty, false } - if entry.HasSlidingExpiration() { - entry.SlideExpiration(now) - c.m[key] = entry + if current.HasSlidingExpiration() { + current.SlideExpiration(now) + c.m[key] = current } - c.queue.Add(entry.node) + c.queue.Add(current.node) c.mu.Unlock() - return entry.val, true + return current.val, true } // Set sets the value for the given key. @@ -113,15 +113,15 @@ func (c *Cache[K, V]) Get(key K) (V, bool) { // the Replace method instead. func (c *Cache[K, V]) Set(key K, val V, exp Expiration) { now := time.Now() - entry := entry[K, V]{val: val} - exp.apply(&entry.exp) - entry.SetDefault(now, c.defaultExp, c.sliding) + new := entry[K, V]{val: val} + exp.apply(&new.exp) + new.SetDefault(now, c.defaultExp, c.sliding) c.mu.Lock() // Make sure that the shard is initialized. c.init() - entry.node = c.queue.AddNew(key) + new.node = c.queue.AddNew(key) current, ok := c.m[key] - c.m[key] = entry + c.m[key] = new if ok { c.queue.Remove(current.node) c.mu.Unlock() @@ -161,16 +161,16 @@ func (c *Cache[K, V]) Set(key K, val V, exp Expiration) { // If it encounters an expired entry, the expired entry is evicted. func (c *Cache[K, V]) GetOrSet(key K, val V, exp Expiration) (V, bool) { now := time.Now() - entry := entry[K, V]{val: val} - exp.apply(&entry.exp) - entry.SetDefault(now, c.defaultExp, c.sliding) + new := entry[K, V]{val: val} + exp.apply(&new.exp) + new.SetDefault(now, c.defaultExp, c.sliding) c.mu.Lock() // Make sure that the shard is initialized. c.init() current, ok := c.m[key] if !ok { - entry.node = c.queue.AddNew(key) - c.m[key] = entry + new.node = c.queue.AddNew(key) + c.m[key] = new if c.size <= 0 || c.len() <= c.size { c.mu.Unlock() return val, false @@ -201,8 +201,8 @@ func (c *Cache[K, V]) GetOrSet(key K, val V, exp Expiration) (V, bool) { c.mu.Unlock() return current.val, true } - entry.node = c.queue.AddNew(key) - c.m[key] = entry + new.node = c.queue.AddNew(key) + c.m[key] = new c.mu.Unlock() if c.onEviction != nil { c.onEviction(key, current.val, EvictionReasonExpired) @@ -219,16 +219,16 @@ func (c *Cache[K, V]) GetOrSet(key K, val V, exp Expiration) (V, bool) { // If you want to add or replace an entry, use the Set method instead. func (c *Cache[K, V]) Replace(key K, val V, exp Expiration) bool { now := time.Now() - entry := entry[K, V]{val: val} - exp.apply(&entry.exp) - entry.SetDefault(now, c.defaultExp, c.sliding) + new := entry[K, V]{val: val} + exp.apply(&new.exp) + new.SetDefault(now, c.defaultExp, c.sliding) c.mu.Lock() current, ok := c.m[key] if !ok { c.mu.Unlock() return false } - entry.node = current.node + new.node = current.node c.queue.Remove(current.node) if current.HasExpired(now) { delete(c.m, key) @@ -238,8 +238,8 @@ func (c *Cache[K, V]) Replace(key K, val V, exp Expiration) bool { } return false } - c.queue.Add(entry.node) - c.m[key] = entry + c.queue.Add(new.node) + c.m[key] = new c.mu.Unlock() if c.onEviction != nil { c.onEviction(key, current.val, EvictionReasonReplaced) @@ -282,11 +282,11 @@ func (c *Cache[K, V]) ReplaceWithFunc(key K, f func(V) V, exp Expiration) bool { } return false } - entry := entry[K, V]{val: f(current.val), node: current.node} - exp.apply(&entry.exp) - entry.SetDefault(now, c.defaultExp, c.sliding) - c.queue.Add(entry.node) - c.m[key] = entry + new := entry[K, V]{val: f(current.val), node: current.node} + exp.apply(&new.exp) + new.SetDefault(now, c.defaultExp, c.sliding) + c.queue.Add(new.node) + c.m[key] = new c.mu.Unlock() if c.onEviction != nil { c.onEviction(key, current.val, EvictionReasonReplaced) @@ -305,23 +305,22 @@ func (c *Cache[K, V]) ReplaceWithFunc(key K, f func(V) V, exp Expiration) bool { func (c *Cache[K, V]) Remove(key K) bool { now := time.Now() c.mu.Lock() - entry, ok := c.m[key] + current, ok := c.m[key] if !ok { c.mu.Unlock() return false } delete(c.m, key) - c.queue.Remove(entry.node) + c.queue.Remove(current.node) c.mu.Unlock() - if entry.HasExpired(now) { - if c.onEviction != nil { - c.onEviction(key, entry.val, EvictionReasonExpired) - } - return false + if c.onEviction == nil { + return !current.HasExpired(now) } - if c.onEviction != nil { - c.onEviction(key, entry.val, EvictionReasonRemoved) + if current.HasExpired(now) { + c.onEviction(key, current.val, EvictionReasonExpired) + return false } + c.onEviction(key, current.val, EvictionReasonRemoved) return true } diff --git a/expiration.go b/expiration.go index 6f85da8..67c878f 100644 --- a/expiration.go +++ b/expiration.go @@ -40,6 +40,10 @@ func WithExpirationDate(t time.Time) Expiration { // WithSlidingExpiration returns an Expiration that sets the expiration time to // now + d and sets the sliding expiration to d. +// +// The sliding expiration is the time after which the entry is considered +// expired if it has not been accessed. If the entry has been accessed, +// the expiration time is reset to now + d where now is the time of the access. func WithSlidingExpiration(d time.Duration) Expiration { return expirationf(func(e *expiration) { e.date = time.Now().Add(d).UnixNano()