Skip to content

Commit

Permalink
cache: add simple cache package
Browse files Browse the repository at this point in the history
  • Loading branch information
progrium committed Mar 9, 2024
1 parent a81485b commit de1f296
Show file tree
Hide file tree
Showing 2 changed files with 205 additions and 0 deletions.
108 changes: 108 additions & 0 deletions internal/cache/cache.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package cache

// simplified version of github.com/patrickmn/go-cache

import (
"sync"
"time"
)

type Item struct {
Object interface{}
Expiration int64
}

// Returns true if the item has expired.
func (item Item) Expired() bool {
if item.Expiration == 0 {
return false
}
return time.Now().UnixNano() > item.Expiration
}

const (
// For use with functions that take an expiration time.
NoExpiration time.Duration = -1
// For use with functions that take an expiration time. Equivalent to
// passing in the same expiration duration as was given to New() or
// NewFrom() when the cache was created (e.g. 5 minutes.)
DefaultExpiration time.Duration = 0
)

type C struct {
defaultExpiration time.Duration
items map[string]Item
mu sync.RWMutex
}

func newCache(de time.Duration, m map[string]Item) *C {
if de == 0 {
de = -1
}
c := &C{
defaultExpiration: de,
items: m,
}
return c
}

// Return a new cache with a given default expiration duration and cleanup
// interval. If the expiration duration is less than one (or NoExpiration),
// the items in the cache never expire (by default), and must be deleted
// manually. If the cleanup interval is less than one, expired items are not
// deleted from the cache before calling c.DeleteExpired().
func New(defaultExpiration time.Duration) *C {
items := make(map[string]Item)
return newCache(defaultExpiration, items)
}

func (c *C) Set(k string, x interface{}, d time.Duration) {
// "Inlining" of set
var e int64
if d == DefaultExpiration {
d = c.defaultExpiration
}
if d > 0 {
e = time.Now().Add(d).UnixNano()
}
c.mu.Lock()
c.items[k] = Item{
Object: x,
Expiration: e,
}
// TODO: Calls to mu.Unlock are currently not deferred because defer
// adds ~200 ns (as of go1.)
c.mu.Unlock()
}

func (c *C) Get(k string) (interface{}, bool) {
c.mu.RLock()
// "Inlining" of get and Expired
item, found := c.items[k]
if !found {
c.mu.RUnlock()
return nil, false
}
if item.Expiration > 0 {
if time.Now().UnixNano() > item.Expiration {
c.mu.RUnlock()
go c.DeleteExpired()
return nil, false
}
}
c.mu.RUnlock()
return item.Object, true
}

// Delete all expired items from the cache.
func (c *C) DeleteExpired() {
now := time.Now().UnixNano()
c.mu.Lock()
for k, v := range c.items {
// "Inlining" of expired
if v.Expiration > 0 && now > v.Expiration {
delete(c.items, k)
}
}
c.mu.Unlock()
}
97 changes: 97 additions & 0 deletions internal/cache/cache_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package cache

import (
"testing"
"time"
)

func TestCache(t *testing.T) {
tc := New(DefaultExpiration)

a, found := tc.Get("a")
if found || a != nil {
t.Error("Getting A found value that shouldn't exist:", a)
}

b, found := tc.Get("b")
if found || b != nil {
t.Error("Getting B found value that shouldn't exist:", b)
}

c, found := tc.Get("c")
if found || c != nil {
t.Error("Getting C found value that shouldn't exist:", c)
}

tc.Set("a", 1, DefaultExpiration)
tc.Set("b", "b", DefaultExpiration)
tc.Set("c", 3.5, DefaultExpiration)

x, found := tc.Get("a")
if !found {
t.Error("a was not found while getting a2")
}
if x == nil {
t.Error("x for a is nil")
} else if a2 := x.(int); a2+2 != 3 {
t.Error("a2 (which should be 1) plus 2 does not equal 3; value:", a2)
}

x, found = tc.Get("b")
if !found {
t.Error("b was not found while getting b2")
}
if x == nil {
t.Error("x for b is nil")
} else if b2 := x.(string); b2+"B" != "bB" {
t.Error("b2 (which should be b) plus B does not equal bB; value:", b2)
}

x, found = tc.Get("c")
if !found {
t.Error("c was not found while getting c2")
}
if x == nil {
t.Error("x for c is nil")
} else if c2 := x.(float64); c2+1.2 != 4.7 {
t.Error("c2 (which should be 3.5) plus 1.2 does not equal 4.7; value:", c2)
}
}

func TestCacheTimes(t *testing.T) {
var found bool

tc := New(50 * time.Millisecond)
tc.Set("a", 1, DefaultExpiration)
tc.Set("b", 2, NoExpiration)
tc.Set("c", 3, 20*time.Millisecond)
tc.Set("d", 4, 70*time.Millisecond)

<-time.After(25 * time.Millisecond)
_, found = tc.Get("c")
if found {
t.Error("Found c when it should have been automatically deleted")
}

<-time.After(30 * time.Millisecond)
_, found = tc.Get("a")
if found {
t.Error("Found a when it should have been automatically deleted")
}

_, found = tc.Get("b")
if !found {
t.Error("Did not find b even though it was set to never expire")
}

_, found = tc.Get("d")
if !found {
t.Error("Did not find d even though it was set to expire later than the default")
}

<-time.After(20 * time.Millisecond)
_, found = tc.Get("d")
if found {
t.Error("Found d when it should have been automatically deleted (later than the default)")
}
}

0 comments on commit de1f296

Please sign in to comment.