diff --git a/CONTRIBUTORS b/CONTRIBUTORS index 8a4da4e..27b5896 100644 --- a/CONTRIBUTORS +++ b/CONTRIBUTORS @@ -5,4 +5,5 @@ code was contributed.) Dustin Sallings Jason Mooberry +Matthew Keller Sergey Shepelev diff --git a/README.md b/README.md index c5789cc..2a7770b 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,15 @@ cache can be saved to and loaded from a file (using `c.Items()` to retrieve the items map to serialize, and `NewFrom()` to create a cache from a deserialized one) to recover from downtime quickly. (See the docs for `NewFrom()` for caveats.) +When creating a cache object using `NewWithLRU()`, if you set the maxItems value +above 0, the LRU functionality is enabled. The cache automatically updates a +timestamp every time a given item is retrieved. In the background, the janitor takes +care of deleting items that have expired because of staleness, or are +least-recently-used when the cache is under pressure. Whatever you set your purge +interval to controls when the item will actually be removed from the cache. If you +don't want to use the janitor, and wish to manually purge LRU items, then +`c.DeleteLRU(n)` where `n` is the number of items you'd like to purge. + ### Installation `go get github.com/patrickmn/go-cache` diff --git a/cache.go b/cache.go index 70e4dad..99b6d61 100644 --- a/cache.go +++ b/cache.go @@ -13,6 +13,7 @@ import ( type Item struct { Object interface{} Expiration int64 + Accessed int64 } // Returns true if the item has expired. @@ -23,6 +24,12 @@ func (item Item) Expired() bool { return time.Now().UnixNano() > item.Expiration } +// Return the time at which this item was last accessed. +func (item Item) LastAccessed() *time.Time { + t := time.Unix(0, item.Accessed) + return &t +} + const ( // For use with functions that take an expiration time. NoExpiration time.Duration = -1 @@ -43,6 +50,7 @@ type cache struct { mu sync.RWMutex onEvicted func(string, interface{}) janitor *janitor + maxItems int } // Add an item to the cache, replacing any existing item. If the duration is 0 @@ -50,34 +58,68 @@ type cache struct { // (NoExpiration), the item never expires. func (c *cache) Set(k string, x interface{}, d time.Duration) { // "Inlining" of set - var e int64 + var ( + now time.Time + e int64 + ) if d == DefaultExpiration { d = c.defaultExpiration } if d > 0 { - e = time.Now().Add(d).UnixNano() + now = time.Now() + e = now.Add(d).UnixNano() } - c.mu.Lock() - c.items[k] = Item{ - Object: x, - Expiration: e, + if c.maxItems > 0 { + if d <= 0 { + // d <= 0 means we didn't set now above + now = time.Now() + } + c.mu.Lock() + c.items[k] = Item{ + Object: x, + Expiration: e, + Accessed: now.UnixNano(), + } + // TODO: Calls to mu.Unlock are currently not deferred because + // defer adds ~200 ns (as of go1.) + c.mu.Unlock() + } else { + c.mu.Lock() + c.items[k] = Item{ + Object: x, + Expiration: e, + } + c.mu.Unlock() } - // TODO: Calls to mu.Unlock are currently not deferred because defer - // adds ~200 ns (as of go1.) - c.mu.Unlock() } func (c *cache) set(k string, x interface{}, d time.Duration) { - var e int64 + var ( + now time.Time + e int64 + ) if d == DefaultExpiration { d = c.defaultExpiration } if d > 0 { - e = time.Now().Add(d).UnixNano() + now = time.Now() + e = now.Add(d).UnixNano() } - c.items[k] = Item{ - Object: x, - Expiration: e, + if c.maxItems > 0 { + if d <= 0 { + // d <= 0 means we didn't set now above + now = time.Now() + } + c.items[k] = Item{ + Object: x, + Expiration: e, + Accessed: now.UnixNano(), + } + } else { + c.items[k] = Item{ + Object: x, + Expiration: e, + } } } @@ -118,23 +160,43 @@ func (c *cache) Replace(k string, x interface{}, d time.Duration) error { // Get an item from the cache. Returns the item or nil, and a bool indicating // whether the key was found. func (c *cache) Get(k string) (interface{}, bool) { - c.mu.RLock() + if c.maxItems > 0 { + // LRU enabled; Get implies write + c.mu.Lock() + } else { + // LRU not enabled; Get is read-only + c.mu.RLock() + } // "Inlining" of get and Expired item, found := c.items[k] if !found { - c.mu.RUnlock() + if c.maxItems > 0 { + c.mu.Unlock() + } else { + c.mu.RUnlock() + } return nil, false } if item.Expiration > 0 { if time.Now().UnixNano() > item.Expiration { - c.mu.RUnlock() + if c.maxItems > 0 { + c.mu.Unlock() + } else { + c.mu.RUnlock() + } return nil, false } } - c.mu.RUnlock() + if c.maxItems > 0 { + c.mu.Unlock() + } else { + c.mu.RUnlock() + } return item.Object, true } +// If LRU functionality is being used (and get implies updating item.Accessed,) +// this function must be write-locked. func (c *cache) get(k string) (interface{}, bool) { item, found := c.items[k] if !found { @@ -146,6 +208,10 @@ func (c *cache) get(k string) (interface{}, bool) { return nil, false } } + if c.maxItems > 0 { + item.Accessed = time.Now().UnixNano() + c.items[k] = item + } return item.Object, true } @@ -161,6 +227,9 @@ func (c *cache) Increment(k string, n int64) error { c.mu.Unlock() return fmt.Errorf("Item %s not found", k) } + if c.maxItems > 0 { + v.Accessed = time.Now().UnixNano() + } switch v.Object.(type) { case int: v.Object = v.Object.(int) + int(n) @@ -209,6 +278,9 @@ func (c *cache) IncrementFloat(k string, n float64) error { c.mu.Unlock() return fmt.Errorf("Item %s not found", k) } + if c.maxItems > 0 { + v.Accessed = time.Now().UnixNano() + } switch v.Object.(type) { case float32: v.Object = v.Object.(float32) + float32(n) @@ -233,6 +305,9 @@ func (c *cache) IncrementInt(k string, n int) (int, error) { c.mu.Unlock() return 0, fmt.Errorf("Item %s not found", k) } + if c.maxItems > 0 { + v.Accessed = time.Now().UnixNano() + } rv, ok := v.Object.(int) if !ok { c.mu.Unlock() @@ -255,6 +330,9 @@ func (c *cache) IncrementInt8(k string, n int8) (int8, error) { c.mu.Unlock() return 0, fmt.Errorf("Item %s not found", k) } + if c.maxItems > 0 { + v.Accessed = time.Now().UnixNano() + } rv, ok := v.Object.(int8) if !ok { c.mu.Unlock() @@ -277,6 +355,9 @@ func (c *cache) IncrementInt16(k string, n int16) (int16, error) { c.mu.Unlock() return 0, fmt.Errorf("Item %s not found", k) } + if c.maxItems > 0 { + v.Accessed = time.Now().UnixNano() + } rv, ok := v.Object.(int16) if !ok { c.mu.Unlock() @@ -299,6 +380,9 @@ func (c *cache) IncrementInt32(k string, n int32) (int32, error) { c.mu.Unlock() return 0, fmt.Errorf("Item %s not found", k) } + if c.maxItems > 0 { + v.Accessed = time.Now().UnixNano() + } rv, ok := v.Object.(int32) if !ok { c.mu.Unlock() @@ -321,6 +405,9 @@ func (c *cache) IncrementInt64(k string, n int64) (int64, error) { c.mu.Unlock() return 0, fmt.Errorf("Item %s not found", k) } + if c.maxItems > 0 { + v.Accessed = time.Now().UnixNano() + } rv, ok := v.Object.(int64) if !ok { c.mu.Unlock() @@ -343,6 +430,9 @@ func (c *cache) IncrementUint(k string, n uint) (uint, error) { c.mu.Unlock() return 0, fmt.Errorf("Item %s not found", k) } + if c.maxItems > 0 { + v.Accessed = time.Now().UnixNano() + } rv, ok := v.Object.(uint) if !ok { c.mu.Unlock() @@ -365,6 +455,9 @@ func (c *cache) IncrementUintptr(k string, n uintptr) (uintptr, error) { c.mu.Unlock() return 0, fmt.Errorf("Item %s not found", k) } + if c.maxItems > 0 { + v.Accessed = time.Now().UnixNano() + } rv, ok := v.Object.(uintptr) if !ok { c.mu.Unlock() @@ -387,6 +480,9 @@ func (c *cache) IncrementUint8(k string, n uint8) (uint8, error) { c.mu.Unlock() return 0, fmt.Errorf("Item %s not found", k) } + if c.maxItems > 0 { + v.Accessed = time.Now().UnixNano() + } rv, ok := v.Object.(uint8) if !ok { c.mu.Unlock() @@ -409,6 +505,9 @@ func (c *cache) IncrementUint16(k string, n uint16) (uint16, error) { c.mu.Unlock() return 0, fmt.Errorf("Item %s not found", k) } + if c.maxItems > 0 { + v.Accessed = time.Now().UnixNano() + } rv, ok := v.Object.(uint16) if !ok { c.mu.Unlock() @@ -431,6 +530,9 @@ func (c *cache) IncrementUint32(k string, n uint32) (uint32, error) { c.mu.Unlock() return 0, fmt.Errorf("Item %s not found", k) } + if c.maxItems > 0 { + v.Accessed = time.Now().UnixNano() + } rv, ok := v.Object.(uint32) if !ok { c.mu.Unlock() @@ -453,6 +555,9 @@ func (c *cache) IncrementUint64(k string, n uint64) (uint64, error) { c.mu.Unlock() return 0, fmt.Errorf("Item %s not found", k) } + if c.maxItems > 0 { + v.Accessed = time.Now().UnixNano() + } rv, ok := v.Object.(uint64) if !ok { c.mu.Unlock() @@ -475,6 +580,9 @@ func (c *cache) IncrementFloat32(k string, n float32) (float32, error) { c.mu.Unlock() return 0, fmt.Errorf("Item %s not found", k) } + if c.maxItems > 0 { + v.Accessed = time.Now().UnixNano() + } rv, ok := v.Object.(float32) if !ok { c.mu.Unlock() @@ -497,6 +605,9 @@ func (c *cache) IncrementFloat64(k string, n float64) (float64, error) { c.mu.Unlock() return 0, fmt.Errorf("Item %s not found", k) } + if c.maxItems > 0 { + v.Accessed = time.Now().UnixNano() + } rv, ok := v.Object.(float64) if !ok { c.mu.Unlock() @@ -523,6 +634,9 @@ func (c *cache) Decrement(k string, n int64) error { c.mu.Unlock() return fmt.Errorf("Item not found") } + if c.maxItems > 0 { + v.Accessed = time.Now().UnixNano() + } switch v.Object.(type) { case int: v.Object = v.Object.(int) - int(n) @@ -571,6 +685,9 @@ func (c *cache) DecrementFloat(k string, n float64) error { c.mu.Unlock() return fmt.Errorf("Item %s not found", k) } + if c.maxItems > 0 { + v.Accessed = time.Now().UnixNano() + } switch v.Object.(type) { case float32: v.Object = v.Object.(float32) - float32(n) @@ -595,6 +712,9 @@ func (c *cache) DecrementInt(k string, n int) (int, error) { c.mu.Unlock() return 0, fmt.Errorf("Item %s not found", k) } + if c.maxItems > 0 { + v.Accessed = time.Now().UnixNano() + } rv, ok := v.Object.(int) if !ok { c.mu.Unlock() @@ -617,6 +737,9 @@ func (c *cache) DecrementInt8(k string, n int8) (int8, error) { c.mu.Unlock() return 0, fmt.Errorf("Item %s not found", k) } + if c.maxItems > 0 { + v.Accessed = time.Now().UnixNano() + } rv, ok := v.Object.(int8) if !ok { c.mu.Unlock() @@ -639,6 +762,9 @@ func (c *cache) DecrementInt16(k string, n int16) (int16, error) { c.mu.Unlock() return 0, fmt.Errorf("Item %s not found", k) } + if c.maxItems > 0 { + v.Accessed = time.Now().UnixNano() + } rv, ok := v.Object.(int16) if !ok { c.mu.Unlock() @@ -661,6 +787,9 @@ func (c *cache) DecrementInt32(k string, n int32) (int32, error) { c.mu.Unlock() return 0, fmt.Errorf("Item %s not found", k) } + if c.maxItems > 0 { + v.Accessed = time.Now().UnixNano() + } rv, ok := v.Object.(int32) if !ok { c.mu.Unlock() @@ -683,6 +812,9 @@ func (c *cache) DecrementInt64(k string, n int64) (int64, error) { c.mu.Unlock() return 0, fmt.Errorf("Item %s not found", k) } + if c.maxItems > 0 { + v.Accessed = time.Now().UnixNano() + } rv, ok := v.Object.(int64) if !ok { c.mu.Unlock() @@ -705,6 +837,9 @@ func (c *cache) DecrementUint(k string, n uint) (uint, error) { c.mu.Unlock() return 0, fmt.Errorf("Item %s not found", k) } + if c.maxItems > 0 { + v.Accessed = time.Now().UnixNano() + } rv, ok := v.Object.(uint) if !ok { c.mu.Unlock() @@ -727,6 +862,9 @@ func (c *cache) DecrementUintptr(k string, n uintptr) (uintptr, error) { c.mu.Unlock() return 0, fmt.Errorf("Item %s not found", k) } + if c.maxItems > 0 { + v.Accessed = time.Now().UnixNano() + } rv, ok := v.Object.(uintptr) if !ok { c.mu.Unlock() @@ -749,6 +887,9 @@ func (c *cache) DecrementUint8(k string, n uint8) (uint8, error) { c.mu.Unlock() return 0, fmt.Errorf("Item %s not found", k) } + if c.maxItems > 0 { + v.Accessed = time.Now().UnixNano() + } rv, ok := v.Object.(uint8) if !ok { c.mu.Unlock() @@ -771,6 +912,9 @@ func (c *cache) DecrementUint16(k string, n uint16) (uint16, error) { c.mu.Unlock() return 0, fmt.Errorf("Item %s not found", k) } + if c.maxItems > 0 { + v.Accessed = time.Now().UnixNano() + } rv, ok := v.Object.(uint16) if !ok { c.mu.Unlock() @@ -793,6 +937,9 @@ func (c *cache) DecrementUint32(k string, n uint32) (uint32, error) { c.mu.Unlock() return 0, fmt.Errorf("Item %s not found", k) } + if c.maxItems > 0 { + v.Accessed = time.Now().UnixNano() + } rv, ok := v.Object.(uint32) if !ok { c.mu.Unlock() @@ -815,6 +962,9 @@ func (c *cache) DecrementUint64(k string, n uint64) (uint64, error) { c.mu.Unlock() return 0, fmt.Errorf("Item %s not found", k) } + if c.maxItems > 0 { + v.Accessed = time.Now().UnixNano() + } rv, ok := v.Object.(uint64) if !ok { c.mu.Unlock() @@ -837,6 +987,9 @@ func (c *cache) DecrementFloat32(k string, n float32) (float32, error) { c.mu.Unlock() return 0, fmt.Errorf("Item %s not found", k) } + if c.maxItems > 0 { + v.Accessed = time.Now().UnixNano() + } rv, ok := v.Object.(float32) if !ok { c.mu.Unlock() @@ -859,6 +1012,9 @@ func (c *cache) DecrementFloat64(k string, n float64) (float64, error) { c.mu.Unlock() return 0, fmt.Errorf("Item %s not found", k) } + if c.maxItems > 0 { + v.Accessed = time.Now().UnixNano() + } rv, ok := v.Object.(float64) if !ok { c.mu.Unlock() @@ -875,9 +1031,10 @@ func (c *cache) DecrementFloat64(k string, n float64) (float64, error) { func (c *cache) Delete(k string) { c.mu.Lock() v, evicted := c.delete(k) + evictFunc := c.onEvicted c.mu.Unlock() if evicted { - c.onEvicted(k, v) + evictFunc(k, v) } } @@ -902,8 +1059,9 @@ func (c *cache) DeleteExpired() { var evictedItems []keyAndValue now := time.Now().UnixNano() c.mu.Lock() + evictFunc := c.onEvicted for k, v := range c.items { - // "Inlining" of expired + // "Inlining" of Expired if v.Expiration > 0 && now > v.Expiration { ov, evicted := c.delete(k) if evicted { @@ -913,7 +1071,7 @@ func (c *cache) DeleteExpired() { } c.mu.Unlock() for _, v := range evictedItems { - c.onEvicted(v.key, v.value) + evictFunc(v.key, v.value) } } @@ -926,6 +1084,74 @@ func (c *cache) OnEvicted(f func(string, interface{})) { c.mu.Unlock() } +// Delete some of the oldest items in the cache if the soft size limit has been +// exceeded. +func (c *cache) DeleteLRU() { + c.mu.Lock() + var ( + overCount = c.itemCount() - c.maxItems + evicted []keyAndValue + evictFunc = c.onEvicted + ) + evicted = c.deleteLRUAmount(overCount) + c.mu.Unlock() + for _, v := range evicted { + evictFunc(v.key, v.value) + } +} + +// Delete a number of the oldest items from the cache. +func (c *cache) DeleteLRUAmount(numItems int) { + c.mu.Lock() + defer c.mu.Unlock() + c.deleteLRUAmount(numItems) +} + +func (c *cache) deleteLRUAmount(numItems int) []keyAndValue { + if numItems <= 0 { + return nil + } + var ( + lastTime int64 = 0 + lastItems = make([]string, numItems) // Ring buffer + liCount = 0 + full = false + evictedItems = make([]keyAndValue, 0, numItems) + now = time.Now().UnixNano() + ) + for k, v := range c.items { + // "Inlining" of !Expired + if v.Expiration == 0 || now <= v.Expiration { + if full == false || v.Accessed < lastTime { + // We found a least-recently-used item, or our + // purge buffer isn't full yet + lastTime = v.Accessed + // Append it to the buffer, or start overwriting + // it + if liCount < numItems { + lastItems[liCount] = k + liCount++ + } else { + lastItems[0] = k + liCount = 1 + full = true + } + } + } + } + if lastTime > 0 { + for _, v := range lastItems { + if v != "" { + ov, evicted := c.delete(v) + if evicted { + evictedItems = append(evictedItems, keyAndValue{v, ov}) + } + } + } + } + return evictedItems +} + // Write the cache's items (using Gob) to an io.Writer. // // NOTE: This method is deprecated in favor of c.Items() and NewFrom() (see the @@ -1031,6 +1257,14 @@ func (c *cache) ItemCount() int { return n } +// Returns the number of items in the cache without locking. This may include +// items that have expired, but have not yet been cleaned up. Equivalent to +// len(c.Items()). +func (c *cache) itemCount() int { + n := len(c.items) + return n +} + // Delete all items from the cache. func (c *cache) Flush() { c.mu.Lock() @@ -1050,6 +1284,9 @@ func (j *janitor) Run(c *cache) { select { case <-ticker.C: c.DeleteExpired() + if c.maxItems > 0 { + c.DeleteLRU() + } case <-j.stop: ticker.Stop() return @@ -1069,19 +1306,20 @@ func runJanitor(c *cache, ci time.Duration) { go j.Run(c) } -func newCache(de time.Duration, m map[string]Item) *cache { +func newCache(de time.Duration, m map[string]Item, mi int) *cache { if de == 0 { de = -1 } c := &cache{ defaultExpiration: de, + maxItems: mi, items: m, } return c } -func newCacheWithJanitor(de time.Duration, ci time.Duration, m map[string]Item) *Cache { - c := newCache(de, m) +func newCacheWithJanitor(de time.Duration, ci time.Duration, m map[string]Item, mi int) *Cache { + c := newCache(de, m, mi) // This trick ensures that the janitor goroutine (which--granted it // was enabled--is running DeleteExpired on c forever) does not keep // the returned C object from being garbage collected. When it is @@ -1102,7 +1340,22 @@ func newCacheWithJanitor(de time.Duration, ci time.Duration, m map[string]Item) // deleted from the cache before calling c.DeleteExpired(). func New(defaultExpiration, cleanupInterval time.Duration) *Cache { items := make(map[string]Item) - return newCacheWithJanitor(defaultExpiration, cleanupInterval, items) + return newCacheWithJanitor(defaultExpiration, cleanupInterval, items, 0) +} + +// Return a new cache with a given default expiration duration, cleanup +// interval, and maximum-ish number of items. If the expiration duration +// is less than one (or NoExpiration), the items in the cache never expire +// (by default), and must be deleted manually. If the cleanup interval is +// less than one, expired items are not deleted from the cache before +// calling c.DeleteExpired(), c.DeleteLRU(), or c.DeleteLRUAmount(). If maxItems +// is not greater than zero, then there will be no soft cap on the number of +// items in the cache. +// +// Using the LRU functionality makes Get() a slower, write-locked operation. +func NewWithLRU(defaultExpiration, cleanupInterval time.Duration, maxItems int) *Cache { + items := make(map[string]Item) + return newCacheWithJanitor(defaultExpiration, cleanupInterval, items, maxItems) } // Return a new cache with a given default expiration duration and cleanup @@ -1127,5 +1380,10 @@ func New(defaultExpiration, cleanupInterval time.Duration) *Cache { // map retrieved with c.Items(), and to register those same types before // decoding a blob containing an items map. func NewFrom(defaultExpiration, cleanupInterval time.Duration, items map[string]Item) *Cache { - return newCacheWithJanitor(defaultExpiration, cleanupInterval, items) + return newCacheWithJanitor(defaultExpiration, cleanupInterval, items, 0) +} + +// Similar to NewFrom, but creates a cache with LRU functionality enabled. +func NewFromWithLRU(defaultExpiration, cleanupInterval time.Duration, items map[string]Item, maxItems int) *Cache { + return newCacheWithJanitor(defaultExpiration, cleanupInterval, items, maxItems) } diff --git a/cache_test.go b/cache_test.go index 6e81693..1308898 100644 --- a/cache_test.go +++ b/cache_test.go @@ -1224,6 +1224,41 @@ func TestDecrementUnderflowUint(t *testing.T) { } } +// TODO: Ring buffer is more efficient but doesn't guarantee that the actually +// oldest items are removed, just some old items. This shouldn't be significant +// for large caches, but we can't test it easily. +// +// func TestDeleteLRU(t *testing.T) { +// tc := NewWithLRU(1*time.Second, 0, 1) +// tc.Set("foo", 0, DefaultExpiration) +// tc.Set("bar", 1, DefaultExpiration) +// tc.Set("baz", 2, DefaultExpiration) +// tc.Get("foo") +// tc.Get("baz") +// time.Sleep(5 * time.Millisecond) +// tc.Get("bar") +// // Bar was accessed most recently, and should be the only value that +// // stays. +// tc.DeleteLRU() +// if tc.ItemCount() != 1 { +// t.Error("tc.ItemCount() is not 1") +// } +// if _, found := tc.Get("bar"); !found { +// t.Error("bar was not found") +// } +// } + +func TestDeleteLRU(t *testing.T) { + tc := NewWithLRU(1*time.Second, 0, 1) + tc.Set("foo", 0, DefaultExpiration) + tc.Set("bar", 1, DefaultExpiration) + tc.Set("baz", 2, DefaultExpiration) + tc.DeleteLRU() + if tc.ItemCount() != 1 { + t.Error("tc.ItemCount() is not 1") + } +} + func TestOnEvicted(t *testing.T) { tc := New(DefaultExpiration, 0) tc.Set("foo", 3, DefaultExpiration) @@ -1443,6 +1478,24 @@ func benchmarkCacheGet(b *testing.B, exp time.Duration) { } } +func BenchmarkCacheWithLRUGetExpiring(b *testing.B) { + benchmarkCacheWithLRUGet(b, 5*time.Minute, 10) +} + +func BenchmarkCacheWithLRUGetNotExpiring(b *testing.B) { + benchmarkCacheWithLRUGet(b, NoExpiration, 10) +} + +func benchmarkCacheWithLRUGet(b *testing.B, exp time.Duration, max int) { + b.StopTimer() + tc := NewWithLRU(exp, 0, max) + tc.Set("foo", "bar", DefaultExpiration) + b.StartTimer() + for i := 0; i < b.N; i++ { + tc.Get("foo") + } +} + func BenchmarkRWMutexMapGet(b *testing.B) { b.StopTimer() m := map[string]string{