Initial commit
This commit is contained in:
commit
3088a9aad8
|
@ -0,0 +1,7 @@
|
||||||
|
_test*
|
||||||
|
src/_test*
|
||||||
|
_obj/
|
||||||
|
*.5
|
||||||
|
*.6
|
||||||
|
*.8
|
||||||
|
*.out
|
|
@ -0,0 +1,7 @@
|
||||||
|
include $(GOROOT)/src/Make.inc
|
||||||
|
|
||||||
|
TARG=github.com/pmylund/go-cache
|
||||||
|
GOFILES=\
|
||||||
|
cache.go\
|
||||||
|
|
||||||
|
include $(GOROOT)/src/Make.pkg
|
|
@ -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
|
||||||
|
}
|
|
@ -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)")
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue