From 77765a3f11edd0e41e0849be49c4468f7cdaa544 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Sat, 25 Oct 2014 17:15:47 +0700 Subject: [PATCH] Get now returns the *Item rather than the item's value. Get no longer actively purges stale items. Combining these two changes, CCache can now be used to implement both of Varnish's grace and saint mode. --- cache.go | 33 ++++++++++++--------------------- cache_test.go | 12 ++++++------ item.go | 29 +++++++++++++++++++++++++++++ item_test.go | 24 ++++++++++++++++++++++++ layeredcache.go | 32 ++++++++++++-------------------- layeredcache_test.go | 34 +++++++++++++++++++--------------- readme.md | 26 ++++++++++++++++++++++---- 7 files changed, 124 insertions(+), 66 deletions(-) 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()`: