Merge pull request #8 from jdeppe-pivotal/master
Add a SecondaryCache which exposes the secondary part of a LayeredCache
This commit is contained in:
@@ -11,13 +11,21 @@ type layeredBucket struct {
|
||||
}
|
||||
|
||||
func (b *layeredBucket) get(primary, secondary string) *Item {
|
||||
bucket := b.getSecondaryBucket(primary)
|
||||
if bucket == nil {
|
||||
return nil
|
||||
}
|
||||
return bucket.get(secondary)
|
||||
}
|
||||
|
||||
func (b *layeredBucket) getSecondaryBucket(primary string) *bucket {
|
||||
b.RLock()
|
||||
bucket, exists := b.buckets[primary]
|
||||
b.RUnlock()
|
||||
if exists == false {
|
||||
return nil
|
||||
}
|
||||
return bucket.get(secondary)
|
||||
return bucket
|
||||
}
|
||||
|
||||
func (b *layeredBucket) set(primary, secondary string, value interface{}, duration time.Duration) (*Item, *Item) {
|
||||
|
||||
@@ -64,6 +64,24 @@ func (c *LayeredCache) Get(primary, secondary string) *Item {
|
||||
return item
|
||||
}
|
||||
|
||||
// Get the secondary cache for a given primary key. This operation will
|
||||
// never return nil. In the case where the primary key does not exist, a
|
||||
// new, underlying, empty bucket will be created and returned.
|
||||
func (c *LayeredCache) GetOrCreateSecondaryCache(primary string) *SecondaryCache {
|
||||
primaryBkt := c.bucket(primary)
|
||||
bkt := primaryBkt.getSecondaryBucket(primary)
|
||||
primaryBkt.Lock()
|
||||
if bkt == nil {
|
||||
bkt = &bucket{lookup: make(map[string]*Item)}
|
||||
primaryBkt.buckets[primary] = bkt
|
||||
}
|
||||
primaryBkt.Unlock()
|
||||
return &SecondaryCache{
|
||||
bucket: bkt,
|
||||
pCache: c,
|
||||
}
|
||||
}
|
||||
|
||||
// Used when the cache was created with the Track() configuration option.
|
||||
// Avoid otherwise
|
||||
func (c *LayeredCache) TrackingGet(primary, secondary string) TrackedItem {
|
||||
|
||||
12
readme.md
12
readme.md
@@ -151,6 +151,18 @@ cache.Delete("/users/goku", "type:xml")
|
||||
cache.DeleteAll("/users/goku")
|
||||
```
|
||||
|
||||
# SecondaryCache
|
||||
|
||||
In some cases, when using a `LayeredCache`, it may be desirable to always be acting on the secondary portion of the cache entry. This could be the case where the primary key is used as a key elsewhere in your code. The `SecondaryCache` is retrieved with:
|
||||
|
||||
```go
|
||||
cache := ccache.Layered(ccache.Configure())
|
||||
sCache := cache.GetOrCreateSecondaryCache("/users/goku")
|
||||
sCache.Set("type:json", "{value_to_cache}", time.Minute * 5)
|
||||
```
|
||||
|
||||
The semantics for interacting with the `SecondaryCache` are exactly the same as for a regular `Cache`. However, one difference is that `Get` will not return nil, but will return an empty 'cache' for a non-existent primary key.
|
||||
|
||||
## Size
|
||||
By default, items added to a cache have a size of 1. This means that if you configure `MaxSize(10000)`, you'll be able to store 10000 items in the cache.
|
||||
|
||||
|
||||
72
secondarycache.go
Normal file
72
secondarycache.go
Normal file
@@ -0,0 +1,72 @@
|
||||
package ccache
|
||||
|
||||
import "time"
|
||||
|
||||
type SecondaryCache struct {
|
||||
bucket *bucket
|
||||
pCache *LayeredCache
|
||||
}
|
||||
|
||||
// Get the secondary key.
|
||||
// The semantics are the same as for LayeredCache.Get
|
||||
func (s *SecondaryCache) Get(secondary string) *Item {
|
||||
return s.bucket.get(secondary)
|
||||
}
|
||||
|
||||
// Set the secondary key to a value.
|
||||
// The semantics are the same as for LayeredCache.Set
|
||||
func (s *SecondaryCache) Set(secondary string, value interface{}, duration time.Duration) *Item {
|
||||
item, existing := s.bucket.set(secondary, value, duration)
|
||||
if existing != nil {
|
||||
s.pCache.deletables <- existing
|
||||
}
|
||||
s.pCache.promote(item)
|
||||
return item
|
||||
}
|
||||
|
||||
// Fetch or set a secondary key.
|
||||
// The semantics are the same as for LayeredCache.Fetch
|
||||
func (s *SecondaryCache) Fetch(secondary string, duration time.Duration, fetch func() (interface{}, error)) (*Item, error) {
|
||||
item := s.Get(secondary)
|
||||
if item != nil {
|
||||
return item, nil
|
||||
}
|
||||
value, err := fetch()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.Set(secondary, value, duration), nil
|
||||
}
|
||||
|
||||
// Delete a secondary key.
|
||||
// The semantics are the same as for LayeredCache.Delete
|
||||
func (s *SecondaryCache) Delete(secondary string) bool {
|
||||
item := s.bucket.delete(secondary)
|
||||
if item != nil {
|
||||
s.pCache.deletables <- item
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Replace a secondary key.
|
||||
// The semantics are the same as for LayeredCache.Replace
|
||||
func (s *SecondaryCache) Replace(secondary string, value interface{}) bool {
|
||||
item := s.Get(secondary)
|
||||
if item == nil {
|
||||
return false
|
||||
}
|
||||
s.Set(secondary, value, item.TTL())
|
||||
return true
|
||||
}
|
||||
|
||||
// Track a secondary key.
|
||||
// The semantics are the same as for LayeredCache.TrackingGet
|
||||
func (c *SecondaryCache) TrackingGet(secondary string) TrackedItem {
|
||||
item := c.Get(secondary)
|
||||
if item == nil {
|
||||
return NilTracked
|
||||
}
|
||||
item.track()
|
||||
return item
|
||||
}
|
||||
105
secondarycache_test.go
Normal file
105
secondarycache_test.go
Normal file
@@ -0,0 +1,105 @@
|
||||
package ccache
|
||||
|
||||
import (
|
||||
. "github.com/karlseguin/expect"
|
||||
"testing"
|
||||
"time"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type SecondaryCacheTests struct{}
|
||||
|
||||
func Test_SecondaryCache(t *testing.T) {
|
||||
Expectify(new(SecondaryCacheTests), t)
|
||||
}
|
||||
|
||||
func (_ SecondaryCacheTests) GetsANonExistantValue() {
|
||||
cache := newLayered().GetOrCreateSecondaryCache("foo")
|
||||
Expect(cache).Not.To.Equal(nil)
|
||||
}
|
||||
|
||||
func (_ SecondaryCacheTests) SetANewValue() {
|
||||
cache := newLayered()
|
||||
cache.Set("spice", "flow", "a value", time.Minute)
|
||||
sCache := cache.GetOrCreateSecondaryCache("spice")
|
||||
Expect(sCache.Get("flow").Value()).To.Equal("a value")
|
||||
Expect(sCache.Get("stop")).To.Equal(nil)
|
||||
}
|
||||
|
||||
func (_ SecondaryCacheTests) ValueCanBeSeenInBothCaches1() {
|
||||
cache := newLayered()
|
||||
cache.Set("spice", "flow", "a value", time.Minute)
|
||||
sCache := cache.GetOrCreateSecondaryCache("spice")
|
||||
sCache.Set("orinoco", "another value", time.Minute)
|
||||
Expect(sCache.Get("orinoco").Value()).To.Equal("another value")
|
||||
Expect(cache.Get("spice", "orinoco").Value()).To.Equal("another value")
|
||||
}
|
||||
|
||||
func (_ SecondaryCacheTests) ValueCanBeSeenInBothCaches2() {
|
||||
cache := newLayered()
|
||||
sCache := cache.GetOrCreateSecondaryCache("spice")
|
||||
sCache.Set("flow", "a value", time.Minute)
|
||||
Expect(sCache.Get("flow").Value()).To.Equal("a value")
|
||||
Expect(cache.Get("spice", "flow").Value()).To.Equal("a value")
|
||||
}
|
||||
|
||||
func (_ SecondaryCacheTests) DeletesAreReflectedInBothCaches() {
|
||||
cache := newLayered()
|
||||
cache.Set("spice", "flow", "a value", time.Minute)
|
||||
cache.Set("spice", "sister", "ghanima", time.Minute)
|
||||
sCache := cache.GetOrCreateSecondaryCache("spice")
|
||||
|
||||
cache.Delete("spice", "flow")
|
||||
Expect(cache.Get("spice", "flow")).To.Equal(nil)
|
||||
Expect(sCache.Get("flow")).To.Equal(nil)
|
||||
|
||||
sCache.Delete("sister")
|
||||
Expect(cache.Get("spice", "sister")).To.Equal(nil)
|
||||
Expect(sCache.Get("sister")).To.Equal(nil)
|
||||
}
|
||||
|
||||
func (_ SecondaryCacheTests) ReplaceDoesNothingIfKeyDoesNotExist() {
|
||||
cache := newLayered()
|
||||
sCache := cache.GetOrCreateSecondaryCache("spice")
|
||||
Expect(sCache.Replace("flow", "value-a")).To.Equal(false)
|
||||
Expect(cache.Get("spice", "flow")).To.Equal(nil)
|
||||
}
|
||||
|
||||
func (_ SecondaryCacheTests) ReplaceUpdatesTheValue() {
|
||||
cache := newLayered()
|
||||
cache.Set("spice", "flow", "value-a", time.Minute)
|
||||
sCache := cache.GetOrCreateSecondaryCache("spice")
|
||||
Expect(sCache.Replace("flow", "value-b")).To.Equal(true)
|
||||
Expect(cache.Get("spice", "flow").Value().(string)).To.Equal("value-b")
|
||||
}
|
||||
|
||||
func (_ SecondaryCacheTests) FetchReturnsAnExistingValue() {
|
||||
cache := newLayered()
|
||||
cache.Set("spice", "flow", "value-a", time.Minute)
|
||||
sCache := cache.GetOrCreateSecondaryCache("spice")
|
||||
val, _ := sCache.Fetch("flow", time.Minute, func() (interface{}, error) {return "a fetched value", nil})
|
||||
Expect(val.Value().(string)).To.Equal("value-a")
|
||||
}
|
||||
|
||||
func (_ SecondaryCacheTests) FetchReturnsANewValue() {
|
||||
cache := newLayered()
|
||||
sCache := cache.GetOrCreateSecondaryCache("spice")
|
||||
val, _ := sCache.Fetch("flow", time.Minute, func() (interface{}, error) {return "a fetched value", nil})
|
||||
Expect(val.Value().(string)).To.Equal("a fetched value")
|
||||
}
|
||||
|
||||
func (_ SecondaryCacheTests) TrackerDoesNotCleanupHeldInstance() {
|
||||
cache := Layered(Configure().ItemsToPrune(10).Track())
|
||||
for i := 0; i < 10; i++ {
|
||||
cache.Set(strconv.Itoa(i), "a", i, time.Minute)
|
||||
}
|
||||
sCache := cache.GetOrCreateSecondaryCache("0")
|
||||
item := sCache.TrackingGet("a")
|
||||
time.Sleep(time.Millisecond * 10)
|
||||
cache.gc()
|
||||
Expect(cache.Get("0", "a").Value()).To.Equal(0)
|
||||
Expect(cache.Get("1", "a")).To.Equal(nil)
|
||||
item.Release()
|
||||
cache.gc()
|
||||
Expect(cache.Get("0", "a")).To.Equal(nil)
|
||||
}
|
||||
Reference in New Issue
Block a user