diff --git a/bucket.go b/bucket.go index c519662..8687672 100644 --- a/bucket.go +++ b/bucket.go @@ -5,18 +5,18 @@ import ( "time" ) -type Bucket struct { +type bucket struct { sync.RWMutex lookup map[string]*Item } -func (b *Bucket) get(key string) *Item { +func (b *bucket) get(key string) *Item { b.RLock() defer b.RUnlock() return b.lookup[key] } -func (b *Bucket) set(key string, value interface{}, duration time.Duration) (*Item, bool) { +func (b *bucket) set(key string, value interface{}, duration time.Duration) (*Item, bool) { expires := time.Now().Add(duration).Unix() b.Lock() defer b.Unlock() @@ -30,7 +30,7 @@ func (b *Bucket) set(key string, value interface{}, duration time.Duration) (*It return item, true } -func (b *Bucket) replace(key string, value interface{}) bool { +func (b *bucket) replace(key string, value interface{}) bool { b.Lock() defer b.Unlock() existing, exists := b.lookup[key] @@ -41,7 +41,7 @@ func (b *Bucket) replace(key string, value interface{}) bool { return true } -func (b *Bucket) delete(key string) *Item { +func (b *bucket) delete(key string) *Item { b.Lock() defer b.Unlock() item := b.lookup[key] @@ -49,7 +49,7 @@ func (b *Bucket) delete(key string) *Item { return item } -func (b *Bucket) clear() { +func (b *bucket) clear() { b.Lock() defer b.Unlock() b.lookup = make(map[string]*Item) diff --git a/bucket_test.go b/bucket_test.go index d6d86d4..379852a 100644 --- a/bucket_test.go +++ b/bucket_test.go @@ -63,8 +63,8 @@ func (_ *BucketTests) ReplaceReplacesThevalue() { //not sure how to test that the TTL hasn't changed sort of a sleep.. } -func testBucket() *Bucket { - b := &Bucket{lookup: make(map[string]*Item)} +func testBucket() *bucket { + b := &bucket{lookup: make(map[string]*Item)} b.lookup["power"] = &Item{ key: "power", value: TestValue("9000"), diff --git a/cache.go b/cache.go index 814273f..c300b2b 100644 --- a/cache.go +++ b/cache.go @@ -11,23 +11,25 @@ import ( type Cache struct { *Configuration list *list.List - buckets []*Bucket + buckets []*bucket bucketMask uint32 deletables chan *Item promotables chan *Item } +// Create a new cache with the specified configuration +// See ccache.Configure() for creating a configuration func New(config *Configuration) *Cache { c := &Cache{ list: list.New(), Configuration: config, bucketMask: uint32(config.buckets) - 1, - buckets: make([]*Bucket, config.buckets), + buckets: make([]*bucket, config.buckets), deletables: make(chan *Item, config.deleteBuffer), promotables: make(chan *Item, config.promoteBuffer), } for i := 0; i < int(config.buckets); i++ { - c.buckets[i] = &Bucket{ + c.buckets[i] = &bucket{ lookup: make(map[string]*Item), } } @@ -35,6 +37,10 @@ func New(config *Configuration) *Cache { return c } +// Get an item from the cache. Returns nil if the item wasn't found. +// This can return an expired item. Use item.Expired() to see if the item +// 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) @@ -47,6 +53,8 @@ func (c *Cache) Get(key string) *Item { return item } +// Used when the cache was created with the Track() configuration option. +// Avoid otherwise func (c *Cache) TrackingGet(key string) TrackedItem { item := c.Get(key) if item == nil { @@ -56,6 +64,7 @@ func (c *Cache) TrackingGet(key string) TrackedItem { return item } +// Set the value in the cache for the specified duration func (c *Cache) Set(key string, value interface{}, duration time.Duration) { item, new := c.bucket(key).set(key, value, duration) if new { @@ -65,10 +74,16 @@ func (c *Cache) Set(key string, value interface{}, duration time.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 *Cache) Replace(key string, value interface{}) bool { return c.bucket(key).replace(key, value) } +// 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) { item := c.Get(key) if item != nil { @@ -81,6 +96,7 @@ func (c *Cache) Fetch(key string, duration time.Duration, fetch func() (interfac return value, err } +// Remove the item from the cache, return true if the item was present, false otherwise. func (c *Cache) Delete(key string) bool { item := c.bucket(key).delete(key) if item != nil { @@ -98,12 +114,12 @@ func (c *Cache) Clear() { c.list = list.New() } -func (c *Cache) deleteItem(bucket *Bucket, item *Item) { +func (c *Cache) deleteItem(bucket *bucket, item *Item) { bucket.delete(item.key) //stop other GETs from getting it c.deletables <- item } -func (c *Cache) bucket(key string) *Bucket { +func (c *Cache) bucket(key string) *bucket { h := fnv.New32a() h.Write([]byte(key)) return c.buckets[h.Sum32()&c.bucketMask] diff --git a/configuration.go b/configuration.go index 5e44d09..f06067c 100644 --- a/configuration.go +++ b/configuration.go @@ -10,6 +10,9 @@ type Configuration struct { tracking bool } +// Creates a configuration object with sensible defaults +// Use this as the start of the fluent configuration: +// e.g.: ccache.New(ccache.Configure().MaxItems(10000)) func Configure() *Configuration { return &Configuration{ buckets: 16, @@ -73,7 +76,7 @@ func (c *Configuration) GetsPerPromote(count int32) *Configuration { // Typically, a cache is agnostic about how cached values are use. This is fine // for a typical cache usage, where you fetch an item from the cache, do something -// (write it out to) and nothing else. +// (write it out) and nothing else. // However, if callers are going to keep a reference to a cached item for a long // time, things get messy. Specifically, the cache can evict the item, while diff --git a/layeredbucket.go b/layeredbucket.go index 0934437..370b763 100644 --- a/layeredbucket.go +++ b/layeredbucket.go @@ -5,12 +5,12 @@ import ( "time" ) -type LayeredBucket struct { +type layeredBucket struct { sync.RWMutex - buckets map[string]*Bucket + buckets map[string]*bucket } -func (b *LayeredBucket) get(primary, secondary string) *Item { +func (b *layeredBucket) get(primary, secondary string) *Item { b.RLock() bucket, exists := b.buckets[primary] b.RUnlock() @@ -20,22 +20,22 @@ 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) { +func (b *layeredBucket) set(primary, secondary string, value interface{}, duration time.Duration) (*Item, bool) { b.Lock() - bucket, exists := b.buckets[primary] + bkt, exists := b.buckets[primary] if exists == false { - bucket = &Bucket{lookup: make(map[string]*Item)} - b.buckets[primary] = bucket + bkt = &bucket{lookup: make(map[string]*Item)} + b.buckets[primary] = bkt } b.Unlock() - item, new := bucket.set(secondary, value, duration) + item, new := bkt.set(secondary, value, duration) if new { item.group = primary } return item, new } -func (b *LayeredBucket) replace(primary, secondary string, value interface{}) bool { +func (b *layeredBucket) replace(primary, secondary string, value interface{}) bool { b.Lock() bucket, exists := b.buckets[primary] b.Unlock() @@ -45,7 +45,7 @@ func (b *LayeredBucket) replace(primary, secondary string, value interface{}) bo return bucket.replace(secondary, value) } -func (b *LayeredBucket) delete(primary, secondary string) *Item { +func (b *layeredBucket) delete(primary, secondary string) *Item { b.RLock() bucket, exists := b.buckets[primary] b.RUnlock() @@ -55,7 +55,7 @@ func (b *LayeredBucket) delete(primary, secondary string) *Item { return bucket.delete(secondary) } -func (b *LayeredBucket) deleteAll(primary string, deletables chan *Item) bool { +func (b *layeredBucket) deleteAll(primary string, deletables chan *Item) bool { b.RLock() bucket, exists := b.buckets[primary] b.RUnlock() @@ -76,11 +76,11 @@ func (b *LayeredBucket) deleteAll(primary string, deletables chan *Item) bool { return true } -func (b *LayeredBucket) clear() { +func (b *layeredBucket) clear() { b.Lock() defer b.Unlock() for _, bucket := range b.buckets { bucket.clear() } - b.buckets = make(map[string]*Bucket) + b.buckets = make(map[string]*bucket) } diff --git a/layeredcache.go b/layeredcache.go index b04be74..c17a8cc 100644 --- a/layeredcache.go +++ b/layeredcache.go @@ -11,30 +11,47 @@ import ( type LayeredCache struct { *Configuration list *list.List - buckets []*LayeredBucket + buckets []*layeredBucket bucketMask uint32 deletables chan *Item promotables chan *Item } +// Create a new layered cache with the specified configuration. +// A layered cache used a two keys to identify a value: a primary key +// and a secondary key. Get, Set and Delete require both a primary and +// secondary key. However, DeleteAll requires only a primary key, deleting +// all values that share the same primary key. + +// Layered Cache is useful as an HTTP cache, where an HTTP purge might +// delete multiple variants of the same resource: +// primary key = "user/44" +// secondary key 1 = ".json" +// secondary key 2 = ".xml" + +// See ccache.Configure() for creating a configuration func Layered(config *Configuration) *LayeredCache { c := &LayeredCache{ list: list.New(), Configuration: config, bucketMask: uint32(config.buckets) - 1, - buckets: make([]*LayeredBucket, config.buckets), + buckets: make([]*layeredBucket, config.buckets), deletables: make(chan *Item, config.deleteBuffer), promotables: make(chan *Item, config.promoteBuffer), } for i := 0; i < int(config.buckets); i++ { - c.buckets[i] = &LayeredBucket{ - buckets: make(map[string]*Bucket), + c.buckets[i] = &layeredBucket{ + buckets: make(map[string]*bucket), } } go c.worker() return c } +// Get an item from the cache. Returns nil if the item wasn't found. +// This can return an expired item. Use item.Expired() to see if the item +// 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) @@ -47,6 +64,8 @@ func (c *LayeredCache) Get(primary, secondary string) *Item { return item } +// Used when the cache was created with the Track() configuration option. +// Avoid otherwise func (c *LayeredCache) TrackingGet(primary, secondary string) TrackedItem { item := c.Get(primary, secondary) if item == nil { @@ -56,6 +75,7 @@ func (c *LayeredCache) TrackingGet(primary, secondary string) TrackedItem { return item } +// Set the value in the cache for the specified duration func (c *LayeredCache) Set(primary, secondary string, value interface{}, duration time.Duration) { item, new := c.bucket(primary).set(primary, secondary, value, duration) if new { @@ -65,10 +85,16 @@ func (c *LayeredCache) Set(primary, secondary string, value interface{}, duratio } } +// 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 { return c.bucket(primary).replace(primary, secondary, value) } +// 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) { item := c.Get(primary, secondary) if item != nil { @@ -81,6 +107,7 @@ func (c *LayeredCache) Fetch(primary, secondary string, duration time.Duration, return value, err } +// Remove the item from the cache, return true if the item was present, false otherwise. func (c *LayeredCache) Delete(primary, secondary string) bool { item := c.bucket(primary).delete(primary, secondary) if item != nil { @@ -90,6 +117,7 @@ func (c *LayeredCache) Delete(primary, secondary string) bool { return false } +// Deletes all items that share the same primary key func (c *LayeredCache) DeleteAll(primary string) bool { return c.bucket(primary).deleteAll(primary, c.deletables) } @@ -102,7 +130,7 @@ func (c *LayeredCache) Clear() { c.list = list.New() } -func (c *LayeredCache) bucket(key string) *LayeredBucket { +func (c *LayeredCache) bucket(key string) *layeredBucket { h := fnv.New32a() h.Write([]byte(key)) return c.buckets[h.Sum32()&c.bucketMask]