diff --git a/cache.go b/cache.go index 7d9f72a..4b29e34 100644 --- a/cache.go +++ b/cache.go @@ -35,33 +35,24 @@ func New(config *Configuration) *Cache { return c } -func (c *Cache) Get(key string) interface{} { - if item := c.get(key); item != nil { - return item.value - } - return nil -} - -func (c *Cache) TrackingGet(key string) TrackedItem { - item := c.get(key) - if item == nil { - return NilTracked - } - item.track() - return item -} - -func (c *Cache) get(key string) *Item { +func (c *Cache) Get(key string) *Item { bucket := c.bucket(key) item := bucket.get(key) if item == nil { return nil } - if item.expires < time.Now().Unix() { - c.deleteItem(bucket, item) - return nil + if item.expires > time.Now().Unix() { + c.conditionalPromote(item) } - c.conditionalPromote(item) + return item +} + +func (c *Cache) TrackingGet(key string) TrackedItem { + item := c.Get(key) + if item == nil { + return NilTracked + } + item.track() return item } diff --git a/cache_test.go b/cache_test.go index 190373f..f202e92 100644 --- a/cache_test.go +++ b/cache_test.go @@ -19,7 +19,7 @@ func (c *CacheTests) DeletesAValue() { cache.Set("worm", "sand", time.Minute) cache.Delete("spice") Expect(cache.Get("spice")).To.Equal(nil) - Expect(cache.Get("worm").(string)).To.Equal("sand") + Expect(cache.Get("worm").Value()).To.Equal("sand") } func (c *CacheTests) GCsTheOldestItems() { @@ -31,7 +31,7 @@ func (c *CacheTests) GCsTheOldestItems() { time.Sleep(time.Millisecond * 10) cache.gc() Expect(cache.Get("9")).To.Equal(nil) - Expect(cache.Get("10").(int)).To.Equal(10) + Expect(cache.Get("10").Value()).To.Equal(10) } func (c *CacheTests) PromotedItemsDontGetPruned() { @@ -43,9 +43,9 @@ func (c *CacheTests) PromotedItemsDontGetPruned() { cache.Get("9") time.Sleep(time.Millisecond * 10) cache.gc() - Expect(cache.Get("9").(int)).To.Equal(9) + Expect(cache.Get("9").Value()).To.Equal(9) Expect(cache.Get("10")).To.Equal(nil) - Expect(cache.Get("11").(int)).To.Equal(11) + Expect(cache.Get("11").Value()).To.Equal(11) } func (c *CacheTests) TrackerDoesNotCleanupHeldInstance() { @@ -56,7 +56,7 @@ func (c *CacheTests) TrackerDoesNotCleanupHeldInstance() { item := cache.TrackingGet("0") time.Sleep(time.Millisecond * 10) cache.gc() - Expect(cache.Get("0").(int)).To.Equal(0) + Expect(cache.Get("0").Value()).To.Equal(0) Expect(cache.Get("1")).To.Equal(nil) item.Release() cache.gc() @@ -71,5 +71,5 @@ func (c *CacheTests) RemovesOldestItemWhenFull() { time.Sleep(time.Millisecond * 10) Expect(cache.Get("0")).To.Equal(nil) Expect(cache.Get("1")).To.Equal(nil) - Expect(cache.Get("2").(int)).To.Equal(2) + Expect(cache.Get("2").Value()).To.Equal(2) } diff --git a/item.go b/item.go index cb5ae94..57c3437 100644 --- a/item.go +++ b/item.go @@ -3,11 +3,16 @@ package ccache import ( "container/list" "sync/atomic" + "time" ) + type TrackedItem interface { Value() interface{} Release() + Expired() bool + TTL() time.Duration + Expires() time.Time } type nilItem struct{} @@ -15,6 +20,18 @@ type nilItem struct{} func (n *nilItem) Value() interface{} { return nil } func (n *nilItem) Release() {} +func (i *nilItem) Expired() bool { + return true +} + +func (i *nilItem) TTL() time.Duration { + return time.Minute +} + +func (i *nilItem) Expires() time.Time { + return time.Time{} +} + var NilTracked = new(nilItem) type Item struct { @@ -51,3 +68,15 @@ func (i *Item) track() { func (i *Item) Release() { atomic.AddInt32(&i.refCount, -1) } + +func (i *Item) Expired() bool { + return i.expires < time.Now().Unix() +} + +func (i *Item) TTL() time.Duration { + return time.Second * time.Duration(i.expires - time.Now().Unix()) +} + +func (i *Item) Expires() time.Time { + return time.Unix(i.expires, 0) +} diff --git a/item_test.go b/item_test.go index 27ada58..bf58f10 100644 --- a/item_test.go +++ b/item_test.go @@ -3,6 +3,7 @@ package ccache import ( . "github.com/karlseguin/expect" "testing" + "time" ) type ItemTests struct{} @@ -16,3 +17,26 @@ func (i *ItemTests) Promotability() { Expect(item.shouldPromote(5)).To.Equal(true) Expect(item.shouldPromote(5)).To.Equal(false) } + +func (i *ItemTests) Expired() { + now := time.Now().Unix() + item1 := &Item{expires: now + 1} + item2 := &Item{expires: now - 1} + Expect(item1.Expired()).To.Equal(false) + Expect(item2.Expired()).To.Equal(true) +} + +func (i *ItemTests) TTL() { + now := time.Now().Unix() + item1 := &Item{expires: now + 10} + item2 := &Item{expires: now - 10} + Expect(item1.TTL()).To.Equal(time.Second * 10) + Expect(item2.TTL()).To.Equal(time.Second * -10) +} + + +func (i *ItemTests) Expires() { + now := time.Now().Unix() + item1 := &Item{expires: now + 10} + Expect(item1.Expires().Unix()).To.Equal(now + 10) +} diff --git a/layeredcache.go b/layeredcache.go index 939bdf6..9120673 100644 --- a/layeredcache.go +++ b/layeredcache.go @@ -35,32 +35,24 @@ func Layered(config *Configuration) *LayeredCache { return c } -func (c *LayeredCache) Get(primary, secondary string) interface{} { - if item := c.get(primary, secondary); item != nil { - return item.value - } - return nil -} - -func (c *LayeredCache) TrackingGet(primary, secondary string) TrackedItem { - item := c.get(primary, secondary) - if item == nil { - return NilTracked - } - item.track() - return item -} - -func (c *LayeredCache) get(primary, secondary string) *Item { +func (c *LayeredCache) Get(primary, secondary string) *Item { bucket := c.bucket(primary) item := bucket.get(primary, secondary) if item == nil { return nil } - if item.expires < time.Now().Unix() { - return nil + if item.expires > time.Now().Unix() { + c.conditionalPromote(item) } - c.conditionalPromote(item) + return item +} + +func (c *LayeredCache) TrackingGet(primary, secondary string) TrackedItem { + item := c.Get(primary, secondary) + if item == nil { + return NilTracked + } + item.track() return item } diff --git a/layeredcache_test.go b/layeredcache_test.go index c9e5642..a576d10 100644 --- a/layeredcache_test.go +++ b/layeredcache_test.go @@ -21,7 +21,7 @@ func (l *LayeredCacheTests) GetsANonExistantValue() { func (l *LayeredCacheTests) SetANewValue() { cache := newLayered() cache.Set("spice", "flow", "a value", time.Minute) - Expect(cache.Get("spice", "flow").(string)).To.Equal("a value") + Expect(cache.Get("spice", "flow").Value()).To.Equal("a value") Expect(cache.Get("spice", "stop")).To.Equal(nil) } @@ -30,11 +30,11 @@ func (l *LayeredCacheTests) SetsMultipleValueWithinTheSameLayer() { cache.Set("spice", "flow", "value-a", time.Minute) cache.Set("spice", "must", "value-b", time.Minute) cache.Set("leto", "sister", "ghanima", time.Minute) - Expect(cache.Get("spice", "flow").(string)).To.Equal("value-a") - Expect(cache.Get("spice", "must").(string)).To.Equal("value-b") + Expect(cache.Get("spice", "flow").Value()).To.Equal("value-a") + Expect(cache.Get("spice", "must").Value()).To.Equal("value-b") Expect(cache.Get("spice", "worm")).To.Equal(nil) - Expect(cache.Get("leto", "sister").(string)).To.Equal("ghanima") + Expect(cache.Get("leto", "sister").Value()).To.Equal("ghanima") Expect(cache.Get("leto", "brother")).To.Equal(nil) Expect(cache.Get("baron", "friend")).To.Equal(nil) } @@ -46,9 +46,9 @@ func (l *LayeredCacheTests) DeletesAValue() { cache.Set("leto", "sister", "ghanima", time.Minute) cache.Delete("spice", "flow") Expect(cache.Get("spice", "flow")).To.Equal(nil) - Expect(cache.Get("spice", "must").(string)).To.Equal("value-b") + Expect(cache.Get("spice", "must").Value()).To.Equal("value-b") Expect(cache.Get("spice", "worm")).To.Equal(nil) - Expect(cache.Get("leto", "sister").(string)).To.Equal("ghanima") + Expect(cache.Get("leto", "sister").Value()).To.Equal("ghanima") } func (l *LayeredCacheTests) DeletesALayer() { @@ -60,7 +60,7 @@ func (l *LayeredCacheTests) DeletesALayer() { Expect(cache.Get("spice", "flow")).To.Equal(nil) Expect(cache.Get("spice", "must")).To.Equal(nil) Expect(cache.Get("spice", "worm")).To.Equal(nil) - Expect(cache.Get("leto", "sister").(string)).To.Equal("ghanima") + Expect(cache.Get("leto", "sister").Value()).To.Equal("ghanima") } func (c *LayeredCacheTests) GCsTheOldestItems() { @@ -74,10 +74,10 @@ func (c *LayeredCacheTests) GCsTheOldestItems() { time.Sleep(time.Millisecond * 10) cache.gc() Expect(cache.Get("xx", "a")).To.Equal(nil) - Expect(cache.Get("xx", "b").(int)).To.Equal(9001) + Expect(cache.Get("xx", "b").Value()).To.Equal(9001) Expect(cache.Get("8", "a")).To.Equal(nil) - Expect(cache.Get("9", "a")).To.Equal(9) - Expect(cache.Get("10", "a").(int)).To.Equal(10) + Expect(cache.Get("9", "a").Value()).To.Equal(9) + Expect(cache.Get("10", "a").Value()).To.Equal(10) } func (c *LayeredCacheTests) PromotedItemsDontGetPruned() { @@ -89,9 +89,9 @@ func (c *LayeredCacheTests) PromotedItemsDontGetPruned() { cache.Get("9", "a") time.Sleep(time.Millisecond * 10) cache.gc() - Expect(cache.Get("9", "a").(int)).To.Equal(9) + Expect(cache.Get("9", "a").Value()).To.Equal(9) Expect(cache.Get("10", "a")).To.Equal(nil) - Expect(cache.Get("11", "a").(int)).To.Equal(11) + Expect(cache.Get("11", "a").Value()).To.Equal(11) } func (c *LayeredCacheTests) TrackerDoesNotCleanupHeldInstance() { @@ -102,7 +102,7 @@ func (c *LayeredCacheTests) TrackerDoesNotCleanupHeldInstance() { item := cache.TrackingGet("0", "a") time.Sleep(time.Millisecond * 10) cache.gc() - Expect(cache.Get("0", "a").(int)).To.Equal(0) + Expect(cache.Get("0", "a").Value()).To.Equal(0) Expect(cache.Get("1", "a")).To.Equal(nil) item.Release() cache.gc() @@ -121,10 +121,14 @@ func (c *LayeredCacheTests) RemovesOldestItemWhenFull() { Expect(cache.Get("0", "a")).To.Equal(nil) Expect(cache.Get("1", "a")).To.Equal(nil) Expect(cache.Get("2", "a")).To.Equal(nil) - Expect(cache.Get("3", "a")).To.Equal(3) - Expect(cache.Get("xx", "b")).To.Equal(9001) + Expect(cache.Get("3", "a").Value()).To.Equal(3) + Expect(cache.Get("xx", "b").Value()).To.Equal(9001) } +// func (c *LayeredCacheTests) GetsAnExpiredIten() { + +// } + func newLayered() *LayeredCache { return Layered(Configure()) } diff --git a/readme.md b/readme.md index 7c13ec2..791b50d 100644 --- a/readme.md +++ b/readme.md @@ -14,7 +14,7 @@ First, download the project: go get github.com/karlseguin/ccache ## Configuration -Next, import and create a `ccache` instance: +Next, import and create a `Cache` instance: ```go @@ -45,23 +45,34 @@ Configurations that change the internals of the cache, which aren't as likely to ## Usage -Once the cache is setup, you can `Get`, `Set` and `Delete` items from it. A `Get` returns an `interface{}` which you'll want to cast back to the type of object you stored: +Once the cache is setup, you can `Get`, `Set` and `Delete` items from it. A `Get` returns an `*Item`: +### Get ```go item := cache.Get("user:4") if item == nil { //handle } else { - user := item.(*User) + user := item.Value().(*User) } ``` +The returned `*Item` exposes a number of methods: +* `Value() interface{}` - the value cached +* `Expired() bool` - whether the item is expired or not +* `TTL() time.Duration` - the duration before the item expires (will be a negative value for expired items) +* `Expires() time.Time` - the time the item will expire + +By returning expired items, CCache lets you decide if you want to serve stale content or not. For example, you might decide to serve up slightly stale content (< 30 seconds old) while re-fetching newer data in the background. You might also decide to serve up infinitely stale content if you're unable to get new data from your source. + +### Set `Set` expects the key, value and ttl: ```go cache.Set("user:4", user, time.Minute * 10) ``` +### Fetch There's also a `Fetch` which mixes a `Get` and a `Set`: ```go @@ -71,8 +82,15 @@ item, err := cache.Fetch("user:4", time.Minute * 10, func() (interface{}, error) }) ``` +### Delete +`Delete` expects the key to delete. It's ok to call `Delete` on a non-existant key: + +```go +cache.Delete("user:4") +``` + ## Tracking -ccache supports a special tracking mode which is meant to be used in conjunction with other pieces of your code that maintains a long-lived reference to data. +CCache supports a special tracking mode which is meant to be used in conjunction with other pieces of your code that maintains a long-lived reference to data. When you configure your cache with `Track()`: