Skip to content

Commit

Permalink
Improve expiration documentation (#33)
Browse files Browse the repository at this point in the history
  • Loading branch information
erni27 authored Apr 20, 2023
1 parent e27ff9b commit 5030469
Show file tree
Hide file tree
Showing 3 changed files with 60 additions and 46 deletions.
19 changes: 15 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.

Expand Down
83 changes: 41 additions & 42 deletions cache.go
Original file line number Diff line number Diff line change
@@ -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.
//
Expand Down Expand Up @@ -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.
Expand All @@ -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()
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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)
Expand All @@ -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
}

Expand Down
4 changes: 4 additions & 0 deletions expiration.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down

0 comments on commit 5030469

Please sign in to comment.