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.
This commit is contained in:
Karl Seguin
2014-10-25 17:15:47 +07:00
parent 3a00ce8f0a
commit 77765a3f11
7 changed files with 124 additions and 66 deletions

View File

@@ -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
}

View File

@@ -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)
}

29
item.go
View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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())
}

View File

@@ -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()`: