From 3088a9aad8116f51cbef30db3fe9e049156f6748 Mon Sep 17 00:00:00 2001 From: Patrick Mylund Nielsen Date: Mon, 2 Jan 2012 11:01:04 +0100 Subject: [PATCH] Initial commit --- .gitignore | 7 ++ Makefile | 7 ++ cache.go | 216 ++++++++++++++++++++++++++++++++++++++++++++++++++ cache_test.go | 97 +++++++++++++++++++++++ 4 files changed, 327 insertions(+) create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 cache.go create mode 100644 cache_test.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..028f238 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +_test* +src/_test* +_obj/ +*.5 +*.6 +*.8 +*.out \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..83705fc --- /dev/null +++ b/Makefile @@ -0,0 +1,7 @@ +include $(GOROOT)/src/Make.inc + +TARG=github.com/pmylund/go-cache +GOFILES=\ + cache.go\ + +include $(GOROOT)/src/Make.pkg diff --git a/cache.go b/cache.go new file mode 100644 index 0000000..b93eba6 --- /dev/null +++ b/cache.go @@ -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 +} diff --git a/cache_test.go b/cache_test.go new file mode 100644 index 0000000..28b8ee1 --- /dev/null +++ b/cache_test.go @@ -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)") + } +}