Skip to content

Commit

Permalink
Merge pull request #70 from mailgun/thrawn/develop
Browse files Browse the repository at this point in the history
Added collections.LRUCache.Map()
  • Loading branch information
thrawn01 authored Jul 13, 2020
2 parents 599bfb8 + cb3defd commit a6cd7c7
Show file tree
Hide file tree
Showing 4 changed files with 70 additions and 21 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,4 @@ coverage.out
mfput.log
.idea/
*.iml
.DS_Store
2 changes: 2 additions & 0 deletions collections/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ with the following
* `Keys()` - Get a list of keys at this point in time
* `Stats()` - Returns stats about the current state of the cache
* `AddWithTTL()` - Adds a value to the cache with a expiration time
* `Each()` - Concurrent non blocking access to each item in the cache
* `Map()` - Effecient blocking modification to each item in the cache

TTL is evaluated during calls to `.Get()` if the entry is past the requested TTL `.Get()`
removes the entry from the cache counts a miss and returns not `ok`
Expand Down
56 changes: 35 additions & 21 deletions collections/lru_cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,10 @@ type LRUCache struct {
// A Key may be any value that is comparable. See http://golang.org/ref/spec#Comparison_operators
type Key interface{}

type cacheRecord struct {
key Key
value interface{}
expireAt *clock.Time
type CacheItem struct {
Key Key
Value interface{}
ExpireAt *clock.Time
}

// New creates a new Cache.
Expand All @@ -71,34 +71,34 @@ func NewLRUCache(maxEntries int) *LRUCache {

// Add or Update a value in the cache, return true if the key already existed
func (c *LRUCache) Add(key Key, value interface{}) bool {
return c.addRecord(&cacheRecord{key: key, value: value})
return c.addRecord(&CacheItem{Key: key, Value: value})
}

// Adds a value to the cache with a TTL
func (c *LRUCache) AddWithTTL(key Key, value interface{}, TTL clock.Duration) bool {
expireAt := clock.Now().UTC().Add(TTL)
return c.addRecord(&cacheRecord{
key: key,
value: value,
expireAt: &expireAt,
return c.addRecord(&CacheItem{
Key: key,
Value: value,
ExpireAt: &expireAt,
})
}

// Adds a value to the cache.
func (c *LRUCache) addRecord(record *cacheRecord) bool {
func (c *LRUCache) addRecord(record *CacheItem) bool {
defer c.mutex.Unlock()
c.mutex.Lock()

// If the key already exist, set the new value
if ee, ok := c.cache[record.key]; ok {
if ee, ok := c.cache[record.Key]; ok {
c.ll.MoveToFront(ee)
temp := ee.Value.(*cacheRecord)
temp := ee.Value.(*CacheItem)
*temp = *record
return true
}

ele := c.ll.PushFront(record)
c.cache[record.key] = ele
c.cache[record.Key] = ele
if c.MaxEntries != 0 && c.ll.Len() > c.MaxEntries {
c.removeOldest()
}
Expand All @@ -111,17 +111,17 @@ func (c *LRUCache) Get(key Key) (value interface{}, ok bool) {
c.mutex.Lock()

if ele, hit := c.cache[key]; hit {
entry := ele.Value.(*cacheRecord)
entry := ele.Value.(*CacheItem)

// If the entry has expired, remove it from the cache
if entry.expireAt != nil && entry.expireAt.Before(clock.Now().UTC()) {
if entry.ExpireAt != nil && entry.ExpireAt.Before(clock.Now().UTC()) {
c.removeElement(ele)
c.stats.Miss++
return
}
c.stats.Hit++
c.ll.MoveToFront(ele)
return entry.value, true
return entry.Value, true
}
c.stats.Miss++
return
Expand All @@ -147,10 +147,10 @@ func (c *LRUCache) removeOldest() {

func (c *LRUCache) removeElement(e *list.Element) {
c.ll.Remove(e)
kv := e.Value.(*cacheRecord)
delete(c.cache, kv.key)
kv := e.Value.(*CacheItem)
delete(c.cache, kv.Key)
if c.OnEvicted != nil {
c.OnEvicted(kv.key, kv.value)
c.OnEvicted(kv.Key, kv.Value)
}
}

Expand Down Expand Up @@ -189,8 +189,8 @@ func (c *LRUCache) Peek(key interface{}) (value interface{}, ok bool) {
c.mutex.Lock()

if ele, hit := c.cache[key]; hit {
entry := ele.Value.(*cacheRecord)
return entry.value, true
entry := ele.Value.(*CacheItem)
return entry.Value, true
}
return nil, false
}
Expand Down Expand Up @@ -226,3 +226,17 @@ func (c LRUCache) Each(concurrent int, callBack func(key interface{}, value inte
}
return nil
}

// Map modifies the cache according to the mapping function, If mapping returns false the
// item is removed from the cache and `OnEvicted` is called if defined. Map claims exclusive
// access to the cache; as such concurrent access will block until Map returns.
func (c *LRUCache) Map(mapping func(item *CacheItem) bool) {
defer c.mutex.Unlock()
c.mutex.Lock()

for _, v := range c.cache {
if !mapping(v.Value.(*CacheItem)) {
c.removeElement(v)
}
}
}
32 changes: 32 additions & 0 deletions collections/lru_cache_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,13 @@ This work is derived from github.com/golang/groupcache/lru
package collections_test

import (
"fmt"
"testing"

"github.com/mailgun/holster/v3/clock"
"github.com/mailgun/holster/v3/collections"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestLRUCache(t *testing.T) {
Expand Down Expand Up @@ -99,3 +101,33 @@ func TestLRUCacheEach(t *testing.T) {
assert.Equal(t, int64(0), stats.Miss)
assert.Equal(t, int64(5), stats.Size)
}

func TestLRUCacheMap(t *testing.T) {
cache := collections.NewLRUCache(5)

cache.Add("1", 1)
cache.Add("2", 2)
cache.Add("3", 3)
cache.Add("4", 4)
cache.Add("5", 5)

var count int
cache.Map(func(item *collections.CacheItem) bool {
count++
if v, ok := item.Value.(int); ok {
// Remove value 3
if v == 3 {
return false
}
}
return true
})
assert.Equal(t, 5, count)
assert.Equal(t, 4, cache.Size())

for _, item := range []int{1,2,4,5} {
v, ok := cache.Get(fmt.Sprintf("%d", item))
require.True(t, ok)
assert.Equal(t, item, v.(int))
}
}

0 comments on commit a6cd7c7

Please sign in to comment.