From c1634a4d00cea59c1140540581c1f51916484101 Mon Sep 17 00:00:00 2001 From: Jens Deppe Date: Tue, 1 Nov 2016 09:01:39 -0700 Subject: [PATCH 1/2] Add concept of a SecondaryCache which exposes the secondary part of a LayeredCache --- layeredbucket.go | 10 +++- layeredcache.go | 16 +++++++ readme.md | 12 +++++ secondarycache.go | 72 ++++++++++++++++++++++++++++ secondarycache_test.go | 105 +++++++++++++++++++++++++++++++++++++++++ 5 files changed, 214 insertions(+), 1 deletion(-) create mode 100644 secondarycache.go create mode 100644 secondarycache_test.go diff --git a/layeredbucket.go b/layeredbucket.go index 8cde8c2..88f3def 100644 --- a/layeredbucket.go +++ b/layeredbucket.go @@ -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) { diff --git a/layeredcache.go b/layeredcache.go index 70a2852..777252c 100644 --- a/layeredcache.go +++ b/layeredcache.go @@ -64,6 +64,22 @@ 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) + if bkt == nil { + bkt = &bucket{lookup: make(map[string]*Item)} + primaryBkt.buckets[primary] = bkt + } + 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 { diff --git a/readme.md b/readme.md index 3f00a24..730a31a 100644 --- a/readme.md +++ b/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. diff --git a/secondarycache.go b/secondarycache.go new file mode 100644 index 0000000..03a34c0 --- /dev/null +++ b/secondarycache.go @@ -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)) (interface{}, 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 +} diff --git a/secondarycache_test.go b/secondarycache_test.go new file mode 100644 index 0000000..2ac2453 --- /dev/null +++ b/secondarycache_test.go @@ -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.(*Item).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.(*Item).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) +} From a451d7262c6c404dba4d9a59b4ac488d39de3149 Mon Sep 17 00:00:00 2001 From: Jens Deppe Date: Tue, 1 Nov 2016 23:53:22 -0700 Subject: [PATCH 2/2] Integrate feedback and upstream fixes - Ensure correct locking in GetOrCreateSecondaryCache - Fetch now returns a *Item --- layeredcache.go | 2 ++ secondarycache.go | 2 +- secondarycache_test.go | 4 ++-- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/layeredcache.go b/layeredcache.go index 747661a..e064eed 100644 --- a/layeredcache.go +++ b/layeredcache.go @@ -70,10 +70,12 @@ func (c *LayeredCache) Get(primary, secondary string) *Item { 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, diff --git a/secondarycache.go b/secondarycache.go index 03a34c0..f901fde 100644 --- a/secondarycache.go +++ b/secondarycache.go @@ -26,7 +26,7 @@ func (s *SecondaryCache) Set(secondary string, value interface{}, duration time. // 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)) (interface{}, error) { +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 diff --git a/secondarycache_test.go b/secondarycache_test.go index 2ac2453..8b733c1 100644 --- a/secondarycache_test.go +++ b/secondarycache_test.go @@ -78,14 +78,14 @@ func (_ SecondaryCacheTests) FetchReturnsAnExistingValue() { 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.(*Item).Value().(string)).To.Equal("value-a") + 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.(*Item).Value().(string)).To.Equal("a fetched value") + Expect(val.Value().(string)).To.Equal("a fetched value") } func (_ SecondaryCacheTests) TrackerDoesNotCleanupHeldInstance() {