sm
/
cache
1
0
Fork 0

Initial commit

This commit is contained in:
Patrick Mylund Nielsen 2012-01-02 11:01:04 +01:00
commit 3088a9aad8
4 changed files with 327 additions and 0 deletions

7
.gitignore vendored Normal file
View File

@ -0,0 +1,7 @@
_test*
src/_test*
_obj/
*.5
*.6
*.8
*.out

7
Makefile Normal file
View File

@ -0,0 +1,7 @@
include $(GOROOT)/src/Make.inc
TARG=github.com/pmylund/go-cache
GOFILES=\
cache.go\
include $(GOROOT)/src/Make.pkg

216
cache.go Normal file
View File

@ -0,0 +1,216 @@
package cache
import (
"runtime"
"sync"
"time"
)
// Cache is an in-memory cache similar to memcached that is suitable for applications
// running on a single machine. Any object can be stored, for a given duration or forever,
// and the cache can be used safely by multiple goroutines.
//
// Installation:
// goinstall github.com/pmylund/go-cache
//
// Usage:
// // Create a cache with a default expiration time of 5 minutes, and which purges
// // expired items every 30 seconds
// c := cache.New(5*time.Minute, 30*time.Second)
//
// // Set the value of the key "foo" to "bar", with the default expiration time
// c.Set("foo", "bar", 0)
//
// // Set the value of the key "baz" to "yes", with no expiration time (the item
// // won't be removed until it is re-set, or removed using c.Delete("baz")
// c.Set("baz", "yes", -1)
//
// // Get the string associated with the key "foo" from the cache
// foo, found := c.Get("foo")
// if found {
// fmt.Println(foo)
// }
//
// // Since Go is statically typed, and cache values can be anything, type assertion
// // is needed when values are being passed to functions that don't take arbitrary types,
// // (i.e. interface{}). The simplest way to do this for values which will only be passed
// // once is:
// foo, found := c.Get("foo")
// if found {
// MyFunction(foo.(string))
// }
//
// // This gets tedious if the value is used several times in the same function. You
// // might do either of the following instead:
// if x, found := c.Get("foo"); found {
// foo := x.(string)
// ...
// }
// // or
// var foo string
// if x, found := c.Get("foo"); found {
// foo = x.(string)
// }
// // foo can then be passed around freely as a string
//
// // Want performance? Store pointers!
// c.Set("foo", &MyStruct, 0)
// if x, found := c.Get("foo"); found {
// foo := x.(*MyStruct)
// ...
// }
type Cache struct {
*cache
// If this is confusing, see the comment at the bottom of the New() function
}
type cache struct {
DefaultExpiration time.Duration
Items map[string]*Item
mu *sync.Mutex
janitor *janitor
}
type Item struct {
Object interface{}
Expires bool
Expiration *time.Time
}
type janitor struct {
Interval time.Duration
stop chan bool
}
// Adds an item to the cache. If the duration is 0, the cache's default expiration time
// is used. If it is -1, the item never expires.
func (c *cache) Set(key string, x interface{}, d time.Duration) {
c.mu.Lock()
defer c.mu.Unlock()
var e *time.Time
expires := true
if d == 0 {
d = c.DefaultExpiration
}
if d == -1 {
expires = false
} else {
t := time.Now().Add(d)
e = &t
}
c.Items[key] = &Item{
Object: x,
Expires: expires,
Expiration: e,
}
}
// Gets an item from the cache.
func (c *cache) Get(key string) (interface{}, bool) {
c.mu.Lock()
defer c.mu.Unlock()
item, found := c.Items[key]
if !found {
return nil, false
}
if item.Expired() {
delete(c.Items, key)
return nil, false
}
return item.Object, true
}
// Deletes an item from the cache. Does nothing if the item does not exist in the cache.
func (c *cache) Delete(key string) {
c.mu.Lock()
defer c.mu.Unlock()
delete(c.Items, key)
}
// Deletes all expired items from the cache.
func (c *cache) DeleteExpired() {
c.mu.Lock()
defer c.mu.Unlock()
for k, v := range c.Items {
if v.Expired() {
delete(c.Items, k)
}
}
}
// Deletes all items in the cache
func (c *cache) Purge() {
c.mu.Lock()
defer c.mu.Unlock()
c.Items = map[string]*Item{}
}
// Returns true if the item has expired.
func (i *Item) Expired() bool {
if i.Expiration == nil {
return false
}
return i.Expiration.Before(time.Now())
}
func (j *janitor) Run(c *cache) {
j.stop = make(chan bool)
tick := time.Tick(j.Interval)
for {
select {
case <-tick:
c.DeleteExpired()
case <-j.stop:
return
}
}
}
func (j *janitor) Stop() {
j.stop <- true
}
func stopJanitor(c *Cache) {
c.janitor.Stop()
}
// Returns a new cache with a given default expiration duration and default cleanup
// interval. If the expiration duration is less than 1, the items in the cache never expire
// and have to be deleted manually. If the cleanup interval is less than one, expired
// items are not deleted from the cache before their next lookup or before calling
// DeleteExpired.
func New(de, ci time.Duration) *Cache {
if de == 0 {
de = -1
}
c := &cache{
DefaultExpiration: de,
Items: map[string]*Item{},
mu: &sync.Mutex{},
}
if ci > 0 {
j := &janitor{
Interval: ci,
}
c.janitor = j
go j.Run(c)
}
// This trick ensures that the janitor goroutine (which--granted it was enabled--is
// running DeleteExpired on c forever, if it was enabled) does not keep the returned C
// object from being garbage collected. When it is garbage collected, the finalizer stops
// the janitor goroutine, after which c is collected.
C := &Cache{c}
if ci > 0 {
runtime.SetFinalizer(C, stopJanitor)
}
return C
}

97
cache_test.go Normal file
View File

@ -0,0 +1,97 @@
package cache
import (
"testing"
"time"
)
func TestCache(t *testing.T) {
tc := New(0, 0)
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, 0)
tc.Set("b", "b", 0)
tc.Set("c", 3.5, 0)
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, 1*time.Millisecond)
tc.Set("a", 1, 0)
tc.Set("b", 2, -1)
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)")
}
}