Compare commits
13 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
8adbb5637b | ||
![]() |
2f6b517f7b | ||
![]() |
162d4e27ca | ||
![]() |
ddcff8e624 | ||
![]() |
3665b16e83 | ||
![]() |
d5307b40af | ||
![]() |
74754c77cc | ||
![]() |
bfa769c6b6 | ||
![]() |
41f1a3cfcb | ||
![]() |
f9c7f14b7b | ||
![]() |
6df1e24ae3 | ||
![]() |
557d56ec6f | ||
![]() |
c75dcd4c12 |
42
bucket.go
42
bucket.go
@@ -2,7 +2,6 @@ package ccache
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -17,45 +16,14 @@ func (b *bucket) get(key string) *Item {
|
||||
return b.lookup[key]
|
||||
}
|
||||
|
||||
func (b *bucket) set(key string, value interface{}, duration time.Duration) (*Item, bool, int64) {
|
||||
expires := time.Now().Add(duration).Unix()
|
||||
b.Lock()
|
||||
defer b.Unlock()
|
||||
if existing, exists := b.lookup[key]; exists {
|
||||
existing.value = value
|
||||
existing.expires = expires
|
||||
d := int64(0)
|
||||
if sized, ok := value.(Sized); ok {
|
||||
newSize := sized.Size()
|
||||
d = newSize - existing.size
|
||||
if d != 0 {
|
||||
atomic.StoreInt64(&existing.size, newSize)
|
||||
}
|
||||
}
|
||||
return existing, false, int64(d)
|
||||
}
|
||||
func (b *bucket) set(key string, value interface{}, duration time.Duration) (*Item, *Item) {
|
||||
expires := time.Now().Add(duration).UnixNano()
|
||||
item := newItem(key, value, expires)
|
||||
b.lookup[key] = item
|
||||
return item, true, int64(item.size)
|
||||
}
|
||||
|
||||
func (b *bucket) replace(key string, value interface{}) (bool, int64) {
|
||||
b.Lock()
|
||||
defer b.Unlock()
|
||||
existing, exists := b.lookup[key]
|
||||
if exists == false {
|
||||
return false, 0
|
||||
}
|
||||
d := int64(0)
|
||||
if sized, ok := value.(Sized); ok {
|
||||
newSize := sized.Size()
|
||||
d = newSize - existing.size
|
||||
if d != 0 {
|
||||
atomic.StoreInt64(&existing.size, newSize)
|
||||
}
|
||||
}
|
||||
existing.value = value
|
||||
return true, d
|
||||
existing := b.lookup[key]
|
||||
b.lookup[key] = item
|
||||
return item, existing
|
||||
}
|
||||
|
||||
func (b *bucket) delete(key string) *Item {
|
||||
|
@@ -32,37 +32,20 @@ func (_ *BucketTests) DeleteItemFromBucket() {
|
||||
|
||||
func (_ *BucketTests) SetsANewBucketItem() {
|
||||
bucket := testBucket()
|
||||
item, new, d := bucket.set("spice", TestValue("flow"), time.Minute)
|
||||
item, existing := bucket.set("spice", TestValue("flow"), time.Minute)
|
||||
assertValue(item, "flow")
|
||||
item = bucket.get("spice")
|
||||
assertValue(item, "flow")
|
||||
Expect(new).To.Equal(true)
|
||||
Expect(d).To.Equal(1)
|
||||
Expect(existing).To.Equal(nil)
|
||||
}
|
||||
|
||||
func (_ *BucketTests) SetsAnExistingItem() {
|
||||
bucket := testBucket()
|
||||
item, new, d := bucket.set("power", TestValue("9002"), time.Minute)
|
||||
item, existing := bucket.set("power", TestValue("9001"), time.Minute)
|
||||
assertValue(item, "9002")
|
||||
item = bucket.get("power")
|
||||
assertValue(item, "9002")
|
||||
Expect(new).To.Equal(false)
|
||||
Expect(d).To.Equal(0)
|
||||
}
|
||||
|
||||
func (_ *BucketTests) ReplaceDoesNothingIfKeyDoesNotExist() {
|
||||
bucket := testBucket()
|
||||
Expect(bucket.replace("power", TestValue("9002"))).To.Equal(false)
|
||||
Expect(bucket.get("power")).To.Equal(nil)
|
||||
}
|
||||
|
||||
func (_ *BucketTests) ReplaceReplacesThevalue() {
|
||||
bucket := testBucket()
|
||||
item, _, _ := bucket.set("power", TestValue("9002"), time.Minute)
|
||||
Expect(bucket.replace("power", TestValue("9004"))).To.Equal(true)
|
||||
Expect(item.Value().(string)).To.Equal("9004")
|
||||
Expect(bucket.get("power").Value().(string)).To.Equal("9004")
|
||||
//not sure how to test that the TTL hasn't changed sort of a sleep..
|
||||
assertValue(existing, "9001")
|
||||
}
|
||||
|
||||
func testBucket() *bucket {
|
||||
|
111
cache.go
111
cache.go
@@ -43,13 +43,12 @@ func New(config *Configuration) *Cache {
|
||||
// is expired and item.TTL() to see how long until the item expires (which
|
||||
// will be negative for an already expired item).
|
||||
func (c *Cache) Get(key string) *Item {
|
||||
bucket := c.bucket(key)
|
||||
item := bucket.get(key)
|
||||
item := c.bucket(key).get(key)
|
||||
if item == nil {
|
||||
return nil
|
||||
}
|
||||
if item.expires > time.Now().Unix() {
|
||||
c.conditionalPromote(item)
|
||||
if item.expires > time.Now().UnixNano() {
|
||||
c.promote(item)
|
||||
}
|
||||
return item
|
||||
}
|
||||
@@ -67,41 +66,34 @@ func (c *Cache) TrackingGet(key string) TrackedItem {
|
||||
|
||||
// Set the value in the cache for the specified duration
|
||||
func (c *Cache) Set(key string, value interface{}, duration time.Duration) {
|
||||
item, new, d := c.bucket(key).set(key, value, duration)
|
||||
if new {
|
||||
c.promote(item)
|
||||
} else {
|
||||
c.conditionalPromote(item)
|
||||
}
|
||||
if d != 0 {
|
||||
atomic.AddInt64(&c.size, d)
|
||||
}
|
||||
c.set(key, value, duration)
|
||||
}
|
||||
|
||||
// Replace the value if it exists, does not set if it doesn't.
|
||||
// Returns true if the item existed an was replaced, false otherwise.
|
||||
// Replace does not reset item's TTL nor does it alter its position in the LRU
|
||||
// Replace does not reset item's TTL
|
||||
func (c *Cache) Replace(key string, value interface{}) bool {
|
||||
exists, d := c.bucket(key).replace(key, value)
|
||||
if d != 0 {
|
||||
atomic.AddInt64(&c.size, d)
|
||||
item := c.bucket(key).get(key)
|
||||
if item == nil {
|
||||
return false
|
||||
}
|
||||
return exists
|
||||
c.Set(key, value, item.TTL())
|
||||
return true
|
||||
}
|
||||
|
||||
// Attempts to get the value from the cache and calles fetch on a miss.
|
||||
// If fetch returns an error, no value is cached and the error is returned back
|
||||
// to the caller.
|
||||
func (c *Cache) Fetch(key string, duration time.Duration, fetch func() (interface{}, error)) (interface{}, error) {
|
||||
// Attempts to get the value from the cache and calles fetch on a miss (missing
|
||||
// or stale item). If fetch returns an error, no value is cached and the error
|
||||
// is returned back to the caller.
|
||||
func (c *Cache) Fetch(key string, duration time.Duration, fetch func() (interface{}, error)) (*Item, error) {
|
||||
item := c.Get(key)
|
||||
if item != nil {
|
||||
if item != nil && !item.Expired() {
|
||||
return item, nil
|
||||
}
|
||||
value, err := fetch()
|
||||
if err == nil {
|
||||
c.Set(key, value, duration)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return value, err
|
||||
return c.set(key, value, duration), nil
|
||||
}
|
||||
|
||||
// Remove the item from the cache, return true if the item was present, false otherwise.
|
||||
@@ -123,24 +115,32 @@ func (c *Cache) Clear() {
|
||||
c.list = list.New()
|
||||
}
|
||||
|
||||
// Stops the background worker. Operations performed on the cache after Stop
|
||||
// is called are likely to panic
|
||||
func (c *Cache) Stop() {
|
||||
close(c.promotables)
|
||||
}
|
||||
|
||||
func (c *Cache) deleteItem(bucket *bucket, item *Item) {
|
||||
bucket.delete(item.key) //stop other GETs from getting it
|
||||
c.deletables <- item
|
||||
}
|
||||
|
||||
func (c *Cache) set(key string, value interface{}, duration time.Duration) *Item {
|
||||
item, existing := c.bucket(key).set(key, value, duration)
|
||||
if existing != nil {
|
||||
c.deletables <- existing
|
||||
}
|
||||
c.promote(item)
|
||||
return item
|
||||
}
|
||||
|
||||
func (c *Cache) bucket(key string) *bucket {
|
||||
h := fnv.New32a()
|
||||
h.Write([]byte(key))
|
||||
return c.buckets[h.Sum32()&c.bucketMask]
|
||||
}
|
||||
|
||||
func (c *Cache) conditionalPromote(item *Item) {
|
||||
if item.shouldPromote(c.getsPerPromote) == false {
|
||||
return
|
||||
}
|
||||
c.promote(item)
|
||||
}
|
||||
|
||||
func (c *Cache) promote(item *Item) {
|
||||
c.promotables <- item
|
||||
}
|
||||
@@ -148,19 +148,37 @@ func (c *Cache) promote(item *Item) {
|
||||
func (c *Cache) worker() {
|
||||
for {
|
||||
select {
|
||||
case item := <-c.promotables:
|
||||
if c.doPromote(item) && atomic.LoadInt64(&c.size) > c.maxSize {
|
||||
case item, ok := <-c.promotables:
|
||||
if ok == false {
|
||||
goto drain
|
||||
}
|
||||
if c.doPromote(item) && c.size > c.maxSize {
|
||||
c.gc()
|
||||
}
|
||||
case item := <-c.deletables:
|
||||
atomic.AddInt64(&c.size, -item.size)
|
||||
if item.element == nil {
|
||||
item.promotions = -2
|
||||
} else {
|
||||
c.list.Remove(item.element)
|
||||
}
|
||||
c.doDelete(item)
|
||||
}
|
||||
}
|
||||
|
||||
drain:
|
||||
for {
|
||||
select {
|
||||
case item := <-c.deletables:
|
||||
c.doDelete(item)
|
||||
default:
|
||||
close(c.deletables)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Cache) doDelete(item *Item) {
|
||||
if item.element == nil {
|
||||
item.promotions = -2
|
||||
} else {
|
||||
c.size -= item.size
|
||||
c.list.Remove(item.element)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Cache) doPromote(item *Item) bool {
|
||||
@@ -168,11 +186,15 @@ func (c *Cache) doPromote(item *Item) bool {
|
||||
if item.promotions == -2 {
|
||||
return false
|
||||
}
|
||||
item.promotions = 0
|
||||
if item.element != nil { //not a new item
|
||||
c.list.MoveToFront(item.element)
|
||||
if item.shouldPromote(c.getsPerPromote) {
|
||||
c.list.MoveToFront(item.element)
|
||||
item.promotions = 0
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
c.size += item.size
|
||||
item.element = c.list.PushFront(item)
|
||||
return true
|
||||
}
|
||||
@@ -187,8 +209,9 @@ func (c *Cache) gc() {
|
||||
item := element.Value.(*Item)
|
||||
if c.tracking == false || atomic.LoadInt32(&item.refCount) == 0 {
|
||||
c.bucket(item.key).delete(item.key)
|
||||
atomic.AddInt64(&c.size, -item.size)
|
||||
c.size -= item.size
|
||||
c.list.Remove(element)
|
||||
item.promotions = -2
|
||||
}
|
||||
element = prev
|
||||
}
|
||||
|
@@ -1,10 +1,11 @@
|
||||
package ccache
|
||||
|
||||
import (
|
||||
. "github.com/karlseguin/expect"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
. "github.com/karlseguin/expect"
|
||||
)
|
||||
|
||||
type CacheTests struct{}
|
||||
@@ -22,6 +23,17 @@ func (_ CacheTests) DeletesAValue() {
|
||||
Expect(cache.Get("worm").Value()).To.Equal("sand")
|
||||
}
|
||||
|
||||
func (_ CacheTests) FetchesExpiredItems() {
|
||||
cache := New(Configure())
|
||||
fn := func() (interface{}, error) { return "moo-moo", nil }
|
||||
|
||||
cache.Set("beef", "moo", time.Second*-1)
|
||||
Expect(cache.Get("beef").Value()).To.Equal("moo")
|
||||
|
||||
out, _ := cache.Fetch("beef", time.Second, fn)
|
||||
Expect(out.Value()).To.Equal("moo-moo")
|
||||
}
|
||||
|
||||
func (_ CacheTests) GCsTheOldestItems() {
|
||||
cache := New(Configure().ItemsToPrune(10))
|
||||
for i := 0; i < 500; i++ {
|
||||
@@ -83,22 +95,27 @@ func (_ CacheTests) RemovesOldestItemWhenFullBySizer() {
|
||||
Expect(cache.Get("0")).To.Equal(nil)
|
||||
Expect(cache.Get("1")).To.Equal(nil)
|
||||
Expect(cache.Get("2")).To.Equal(nil)
|
||||
Expect(cache.Get("3").Value().(*SizedItem).id).To.Equal(3)
|
||||
Expect(cache.Get("3")).To.Equal(nil)
|
||||
Expect(cache.Get("4").Value().(*SizedItem).id).To.Equal(4)
|
||||
}
|
||||
|
||||
func (_ CacheTests) SetUpdatesSizeOnDelta() {
|
||||
cache := New(Configure())
|
||||
cache.Set("a", &SizedItem{0, 2}, time.Minute)
|
||||
cache.Set("b", &SizedItem{0, 3}, time.Minute)
|
||||
time.Sleep(time.Millisecond * 5)
|
||||
Expect(cache.size).To.Equal(int64(5))
|
||||
cache.Set("b", &SizedItem{0, 3}, time.Minute)
|
||||
time.Sleep(time.Millisecond * 5)
|
||||
Expect(cache.size).To.Equal(int64(5))
|
||||
cache.Set("b", &SizedItem{0, 4}, time.Minute)
|
||||
time.Sleep(time.Millisecond * 5)
|
||||
Expect(cache.size).To.Equal(int64(6))
|
||||
cache.Set("b", &SizedItem{0, 2}, time.Minute)
|
||||
time.Sleep(time.Millisecond * 5)
|
||||
Expect(cache.size).To.Equal(int64(4))
|
||||
cache.Delete("b")
|
||||
time.Sleep(time.Millisecond * 10)
|
||||
time.Sleep(time.Millisecond * 100)
|
||||
Expect(cache.size).To.Equal(int64(2))
|
||||
}
|
||||
|
||||
@@ -108,6 +125,7 @@ func (_ CacheTests) ReplaceDoesNotchangeSizeIfNotSet() {
|
||||
cache.Set("2", &SizedItem{1, 2}, time.Minute)
|
||||
cache.Set("3", &SizedItem{1, 2}, time.Minute)
|
||||
cache.Replace("4", &SizedItem{1, 2})
|
||||
time.Sleep(time.Millisecond * 5)
|
||||
Expect(cache.size).To.Equal(int64(6))
|
||||
}
|
||||
|
||||
@@ -117,12 +135,15 @@ func (_ CacheTests) ReplaceChangesSize() {
|
||||
cache.Set("2", &SizedItem{1, 2}, time.Minute)
|
||||
|
||||
cache.Replace("2", &SizedItem{1, 2})
|
||||
time.Sleep(time.Millisecond * 5)
|
||||
Expect(cache.size).To.Equal(int64(4))
|
||||
|
||||
cache.Replace("2", &SizedItem{1, 1})
|
||||
time.Sleep(time.Millisecond * 5)
|
||||
Expect(cache.size).To.Equal(int64(3))
|
||||
|
||||
cache.Replace("2", &SizedItem{1, 3})
|
||||
time.Sleep(time.Millisecond * 5)
|
||||
Expect(cache.size).To.Equal(int64(5))
|
||||
}
|
||||
|
||||
|
13
item.go
13
item.go
@@ -60,14 +60,15 @@ func newItem(key string, value interface{}, expires int64) *Item {
|
||||
return &Item{
|
||||
key: key,
|
||||
value: value,
|
||||
promotions: -1,
|
||||
promotions: 0,
|
||||
size: size,
|
||||
expires: expires,
|
||||
}
|
||||
}
|
||||
|
||||
func (i *Item) shouldPromote(getsPerPromote int32) bool {
|
||||
return atomic.AddInt32(&i.promotions, 1) == getsPerPromote
|
||||
i.promotions += 1
|
||||
return i.promotions == getsPerPromote
|
||||
}
|
||||
|
||||
func (i *Item) Value() interface{} {
|
||||
@@ -84,19 +85,19 @@ func (i *Item) Release() {
|
||||
|
||||
func (i *Item) Expired() bool {
|
||||
expires := atomic.LoadInt64(&i.expires)
|
||||
return expires < time.Now().Unix()
|
||||
return expires < time.Now().UnixNano()
|
||||
}
|
||||
|
||||
func (i *Item) TTL() time.Duration {
|
||||
expires := atomic.LoadInt64(&i.expires)
|
||||
return time.Second * time.Duration(expires-time.Now().Unix())
|
||||
return time.Nanosecond * time.Duration(expires-time.Now().UnixNano())
|
||||
}
|
||||
|
||||
func (i *Item) Expires() time.Time {
|
||||
expires := atomic.LoadInt64(&i.expires)
|
||||
return time.Unix(expires, 0)
|
||||
return time.Unix(0, expires)
|
||||
}
|
||||
|
||||
func (i *Item) Extend(duration time.Duration) {
|
||||
atomic.StoreInt64(&i.expires, time.Now().Add(duration).Unix())
|
||||
atomic.StoreInt64(&i.expires, time.Now().Add(duration).UnixNano())
|
||||
}
|
||||
|
28
item_test.go
28
item_test.go
@@ -1,9 +1,11 @@
|
||||
package ccache
|
||||
|
||||
import (
|
||||
. "github.com/karlseguin/expect"
|
||||
"math"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
. "github.com/karlseguin/expect"
|
||||
)
|
||||
|
||||
type ItemTests struct{}
|
||||
@@ -19,29 +21,29 @@ func (_ *ItemTests) Promotability() {
|
||||
}
|
||||
|
||||
func (_ *ItemTests) Expired() {
|
||||
now := time.Now().Unix()
|
||||
item1 := &Item{expires: now + 1}
|
||||
item2 := &Item{expires: now - 1}
|
||||
now := time.Now().UnixNano()
|
||||
item1 := &Item{expires: now + (10 * int64(time.Millisecond))}
|
||||
item2 := &Item{expires: now - (10 * int64(time.Millisecond))}
|
||||
Expect(item1.Expired()).To.Equal(false)
|
||||
Expect(item2.Expired()).To.Equal(true)
|
||||
}
|
||||
|
||||
func (_ *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)
|
||||
now := time.Now().UnixNano()
|
||||
item1 := &Item{expires: now + int64(time.Second)}
|
||||
item2 := &Item{expires: now - int64(time.Second)}
|
||||
Expect(int(math.Ceil(item1.TTL().Seconds()))).To.Equal(1)
|
||||
Expect(int(math.Ceil(item2.TTL().Seconds()))).To.Equal(-1)
|
||||
}
|
||||
|
||||
func (_ *ItemTests) Expires() {
|
||||
now := time.Now().Unix()
|
||||
item := &Item{expires: now + 10}
|
||||
Expect(item.Expires().Unix()).To.Equal(now + 10)
|
||||
now := time.Now().UnixNano()
|
||||
item := &Item{expires: now + (10)}
|
||||
Expect(item.Expires().UnixNano()).To.Equal(now + 10)
|
||||
}
|
||||
|
||||
func (_ *ItemTests) Extend() {
|
||||
item := &Item{expires: time.Now().Unix() + 10}
|
||||
item := &Item{expires: time.Now().UnixNano() + 10}
|
||||
item.Extend(time.Minute * 2)
|
||||
Expect(item.Expires().Unix()).To.Equal(time.Now().Unix() + 120)
|
||||
}
|
||||
|
@@ -20,7 +20,7 @@ func (b *layeredBucket) get(primary, secondary string) *Item {
|
||||
return bucket.get(secondary)
|
||||
}
|
||||
|
||||
func (b *layeredBucket) set(primary, secondary string, value interface{}, duration time.Duration) (*Item, bool, int64) {
|
||||
func (b *layeredBucket) set(primary, secondary string, value interface{}, duration time.Duration) (*Item, *Item) {
|
||||
b.Lock()
|
||||
bkt, exists := b.buckets[primary]
|
||||
if exists == false {
|
||||
@@ -28,21 +28,9 @@ func (b *layeredBucket) set(primary, secondary string, value interface{}, durati
|
||||
b.buckets[primary] = bkt
|
||||
}
|
||||
b.Unlock()
|
||||
item, new, d := bkt.set(secondary, value, duration)
|
||||
if new {
|
||||
item.group = primary
|
||||
}
|
||||
return item, new, d
|
||||
}
|
||||
|
||||
func (b *layeredBucket) replace(primary, secondary string, value interface{}) (bool, int64) {
|
||||
b.Lock()
|
||||
bucket, exists := b.buckets[primary]
|
||||
b.Unlock()
|
||||
if exists == false {
|
||||
return false, 0
|
||||
}
|
||||
return bucket.replace(secondary, value)
|
||||
item, existing := bkt.set(secondary, value, duration)
|
||||
item.group = primary
|
||||
return item, existing
|
||||
}
|
||||
|
||||
func (b *layeredBucket) delete(primary, secondary string) *Item {
|
||||
|
@@ -54,13 +54,12 @@ func Layered(config *Configuration) *LayeredCache {
|
||||
// is expired and item.TTL() to see how long until the item expires (which
|
||||
// will be negative for an already expired item).
|
||||
func (c *LayeredCache) Get(primary, secondary string) *Item {
|
||||
bucket := c.bucket(primary)
|
||||
item := bucket.get(primary, secondary)
|
||||
item := c.bucket(primary).get(primary, secondary)
|
||||
if item == nil {
|
||||
return nil
|
||||
}
|
||||
if item.expires > time.Now().Unix() {
|
||||
c.conditionalPromote(item)
|
||||
if item.expires > time.Now().UnixNano() {
|
||||
c.promote(item)
|
||||
}
|
||||
return item
|
||||
}
|
||||
@@ -78,41 +77,34 @@ func (c *LayeredCache) TrackingGet(primary, secondary string) TrackedItem {
|
||||
|
||||
// Set the value in the cache for the specified duration
|
||||
func (c *LayeredCache) Set(primary, secondary string, value interface{}, duration time.Duration) {
|
||||
item, new, d := c.bucket(primary).set(primary, secondary, value, duration)
|
||||
if new {
|
||||
c.promote(item)
|
||||
} else {
|
||||
c.conditionalPromote(item)
|
||||
}
|
||||
if d != 0 {
|
||||
atomic.AddInt64(&c.size, d)
|
||||
}
|
||||
c.set(primary, secondary, value, duration)
|
||||
}
|
||||
|
||||
// Replace the value if it exists, does not set if it doesn't.
|
||||
// Returns true if the item existed an was replaced, false otherwise.
|
||||
// Replace does not reset item's TTL nor does it alter its position in the LRU
|
||||
func (c *LayeredCache) Replace(primary, secondary string, value interface{}) bool {
|
||||
exists, d := c.bucket(primary).replace(primary, secondary, value)
|
||||
if d != 0 {
|
||||
atomic.AddInt64(&c.size, d)
|
||||
item := c.bucket(primary).get(primary, secondary)
|
||||
if item == nil {
|
||||
return false
|
||||
}
|
||||
return exists
|
||||
c.Set(primary, secondary, value, item.TTL())
|
||||
return true
|
||||
}
|
||||
|
||||
// Attempts to get the value from the cache and calles fetch on a miss.
|
||||
// If fetch returns an error, no value is cached and the error is returned back
|
||||
// to the caller.
|
||||
func (c *LayeredCache) Fetch(primary, secondary string, duration time.Duration, fetch func() (interface{}, error)) (interface{}, error) {
|
||||
func (c *LayeredCache) Fetch(primary, secondary string, duration time.Duration, fetch func() (interface{}, error)) (*Item, error) {
|
||||
item := c.Get(primary, secondary)
|
||||
if item != nil {
|
||||
return item, nil
|
||||
}
|
||||
value, err := fetch()
|
||||
if err == nil {
|
||||
c.Set(primary, secondary, value, duration)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return value, err
|
||||
return c.set(primary, secondary, value, duration), nil
|
||||
}
|
||||
|
||||
// Remove the item from the cache, return true if the item was present, false otherwise.
|
||||
@@ -139,19 +131,21 @@ func (c *LayeredCache) Clear() {
|
||||
c.list = list.New()
|
||||
}
|
||||
|
||||
func (c *LayeredCache) set(primary, secondary string, value interface{}, duration time.Duration) *Item {
|
||||
item, existing := c.bucket(primary).set(primary, secondary, value, duration)
|
||||
if existing != nil {
|
||||
c.deletables <- existing
|
||||
}
|
||||
c.promote(item)
|
||||
return item
|
||||
}
|
||||
|
||||
func (c *LayeredCache) bucket(key string) *layeredBucket {
|
||||
h := fnv.New32a()
|
||||
h.Write([]byte(key))
|
||||
return c.buckets[h.Sum32()&c.bucketMask]
|
||||
}
|
||||
|
||||
func (c *LayeredCache) conditionalPromote(item *Item) {
|
||||
if item.shouldPromote(c.getsPerPromote) == false {
|
||||
return
|
||||
}
|
||||
c.promote(item)
|
||||
}
|
||||
|
||||
func (c *LayeredCache) promote(item *Item) {
|
||||
c.promotables <- item
|
||||
}
|
||||
@@ -160,14 +154,14 @@ func (c *LayeredCache) worker() {
|
||||
for {
|
||||
select {
|
||||
case item := <-c.promotables:
|
||||
if c.doPromote(item) && atomic.LoadInt64(&c.size) > c.maxSize {
|
||||
if c.doPromote(item) && c.size > c.maxSize {
|
||||
c.gc()
|
||||
}
|
||||
case item := <-c.deletables:
|
||||
atomic.AddInt64(&c.size, -item.size)
|
||||
if item.element == nil {
|
||||
item.promotions = -2
|
||||
} else {
|
||||
c.size -= item.size
|
||||
c.list.Remove(item.element)
|
||||
}
|
||||
}
|
||||
@@ -179,12 +173,14 @@ func (c *LayeredCache) doPromote(item *Item) bool {
|
||||
if item.promotions == -2 {
|
||||
return false
|
||||
}
|
||||
|
||||
item.promotions = 0
|
||||
if item.element != nil { //not a new item
|
||||
c.list.MoveToFront(item.element)
|
||||
if item.shouldPromote(c.getsPerPromote) {
|
||||
c.list.MoveToFront(item.element)
|
||||
item.promotions = 0
|
||||
}
|
||||
return false
|
||||
}
|
||||
c.size += item.size
|
||||
item.element = c.list.PushFront(item)
|
||||
return true
|
||||
}
|
||||
@@ -198,9 +194,10 @@ func (c *LayeredCache) gc() {
|
||||
prev := element.Prev()
|
||||
item := element.Value.(*Item)
|
||||
if c.tracking == false || atomic.LoadInt32(&item.refCount) == 0 {
|
||||
atomic.AddInt64(&c.size, -item.size)
|
||||
c.bucket(item.group).delete(item.group, item.key)
|
||||
c.size -= item.size
|
||||
c.list.Remove(element)
|
||||
item.promotions = -2
|
||||
}
|
||||
element = prev
|
||||
}
|
||||
|
@@ -152,20 +152,25 @@ func (_ LayeredCacheTests) RemovesOldestItemWhenFullBySizer() {
|
||||
Expect(cache.Get("pri", "0")).To.Equal(nil)
|
||||
Expect(cache.Get("pri", "1")).To.Equal(nil)
|
||||
Expect(cache.Get("pri", "2")).To.Equal(nil)
|
||||
Expect(cache.Get("pri", "3").Value().(*SizedItem).id).To.Equal(3)
|
||||
Expect(cache.Get("pri", "3")).To.Equal(nil)
|
||||
Expect(cache.Get("pri", "4").Value().(*SizedItem).id).To.Equal(4)
|
||||
}
|
||||
|
||||
func (_ LayeredCacheTests) SetUpdatesSizeOnDelta() {
|
||||
cache := Layered(Configure())
|
||||
cache.Set("pri", "a", &SizedItem{0, 2}, time.Minute)
|
||||
cache.Set("pri", "b", &SizedItem{0, 3}, time.Minute)
|
||||
time.Sleep(time.Millisecond * 5)
|
||||
Expect(cache.size).To.Equal(int64(5))
|
||||
cache.Set("pri", "b", &SizedItem{0, 3}, time.Minute)
|
||||
time.Sleep(time.Millisecond * 5)
|
||||
Expect(cache.size).To.Equal(int64(5))
|
||||
cache.Set("pri", "b", &SizedItem{0, 4}, time.Minute)
|
||||
time.Sleep(time.Millisecond * 5)
|
||||
Expect(cache.size).To.Equal(int64(6))
|
||||
cache.Set("pri", "b", &SizedItem{0, 2}, time.Minute)
|
||||
cache.Set("sec", "b", &SizedItem{0, 3}, time.Minute)
|
||||
time.Sleep(time.Millisecond * 5)
|
||||
Expect(cache.size).To.Equal(int64(7))
|
||||
cache.Delete("pri", "b")
|
||||
time.Sleep(time.Millisecond * 10)
|
||||
@@ -178,6 +183,7 @@ func (_ LayeredCacheTests) ReplaceDoesNotchangeSizeIfNotSet() {
|
||||
cache.Set("pri", "2", &SizedItem{1, 2}, time.Minute)
|
||||
cache.Set("pri", "3", &SizedItem{1, 2}, time.Minute)
|
||||
cache.Replace("sec", "3", &SizedItem{1, 2})
|
||||
time.Sleep(time.Millisecond * 5)
|
||||
Expect(cache.size).To.Equal(int64(6))
|
||||
}
|
||||
|
||||
@@ -187,11 +193,14 @@ func (_ LayeredCacheTests) ReplaceChangesSize() {
|
||||
cache.Set("pri", "2", &SizedItem{1, 2}, time.Minute)
|
||||
|
||||
cache.Replace("pri", "2", &SizedItem{1, 2})
|
||||
time.Sleep(time.Millisecond * 5)
|
||||
Expect(cache.size).To.Equal(int64(4))
|
||||
|
||||
cache.Replace("pri", "2", &SizedItem{1, 1})
|
||||
time.Sleep(time.Millisecond * 5)
|
||||
Expect(cache.size).To.Equal(int64(3))
|
||||
|
||||
cache.Replace("pri", "2", &SizedItem{1, 3})
|
||||
time.Sleep(time.Millisecond * 5)
|
||||
Expect(cache.size).To.Equal(int64(5))
|
||||
}
|
||||
|
@@ -101,6 +101,10 @@ cache.Replace("user:4", user)
|
||||
|
||||
`Replace` returns true if the item existed (and thus was replaced). In the case where the key was not in the cache, the value *is not* inserted and false is returned.
|
||||
|
||||
### Stop
|
||||
The cache's background worker can be stopped by calling `Stop`. Once `Stop` is called
|
||||
the cache should not be used (calls are likely to panic). Stop must be called in order to allow the garbage collector to reap the cache.
|
||||
|
||||
## 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.
|
||||
|
||||
@@ -146,7 +150,11 @@ cache.Delete("/users/goku", "type:xml")
|
||||
// OR
|
||||
cache.DeleteAll("/users/goku")
|
||||
```
|
||||
|
||||
## 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.
|
||||
|
||||
However, if the values you set into the cache have a method `Size() int64`, this size will be used. Note that ccache has an overhead of ~350 bytes per entry, which isn't taken into account. In other words, given a filled up cache, with `MaxSize(4096000)` and items that return a `Size() int64` of 2048, we can expect to find 2000 items (4096000/2048) taking a total space of 4796000 bytes.
|
||||
|
||||
## Want Something Simpler?
|
||||
For a simpler cache, checkout out [rcache](https://github.com/karlseguin/rcache)
|
||||
|
Reference in New Issue
Block a user