Skip to content

Commit

Permalink
Add remove reason signal to OnRemove callback
Browse files Browse the repository at this point in the history
  • Loading branch information
Jacob Shufro committed Apr 21, 2018
1 parent e3e2429 commit 0071311
Show file tree
Hide file tree
Showing 5 changed files with 135 additions and 21 deletions.
11 changes: 8 additions & 3 deletions README.md
Expand Up @@ -46,10 +46,15 @@ config := bigcache.Config {
// if value is reached then the oldest entries can be overridden for the new ones
// 0 value means no size limit
HardMaxCacheSize: 8192,
// callback fired when the oldest entry is removed because of its
// expiration time or no space left for the new entry. Default value is nil which
// means no callback and it prevents from unwrapping the oldest entry.
// callback fired when the oldest entry is removed because of its expiration time or no space left
// for the new entry, or because delete was called. A bitmask representing the reason will be returned.
// Default value is nil which means no callback and it prevents from unwrapping the oldest entry.
OnRemove: nil,
// OnRemoveWithReason is a callback fired when the oldest entry is removed because of its expiration time or no space left
// for the new entry, or because delete was called. A constant representing the reason will be passed through.
// Default value is nil which means no callback and it prevents from unwrapping the oldest entry.
// Ignored if OnRemove is specified.
OnRemoveWithReason: nil,
}

cache, initErr := bigcache.NewBigCache(config)
Expand Down
37 changes: 29 additions & 8 deletions bigcache.go
Expand Up @@ -22,6 +22,19 @@ type BigCache struct {
maxShardSize uint32
}

// RemoveReason is a value used to signal to the user why a particular key was removed in the OnRemove callback.
type RemoveReason uint32

const (
// Expired means the key is past its LifeWindow.
Expired RemoveReason = iota
// NoSpace means the key is the oldest and the cache size was at its maximum when Set was called, or the
// entry exceeded the maximum shard size.
NoSpace
// Deleted means Delete was called and this key was removed as a result.
Deleted
)

// NewBigCache initialize new instance of BigCache
func NewBigCache(config Config) (*BigCache, error) {
return newBigCache(config, &systemClock{})
Expand All @@ -47,11 +60,13 @@ func newBigCache(config Config, clock clock) (*BigCache, error) {
maxShardSize: uint32(config.maximumShardSize()),
}

var onRemove func(wrappedEntry []byte)
if config.OnRemove == nil {
onRemove = cache.notProvidedOnRemove
} else {
var onRemove func(wrappedEntry []byte, reason RemoveReason)
if config.OnRemove != nil {
onRemove = cache.providedOnRemove
} else if config.OnRemoveWithReason != nil {
onRemove = cache.providedOnRemoveWithReason
} else {
onRemove = cache.notProvidedOnRemove
}

for i := 0; i < config.Shards; i++ {
Expand Down Expand Up @@ -128,10 +143,10 @@ func (c *BigCache) Iterator() *EntryInfoIterator {
return newIterator(c)
}

func (c *BigCache) onEvict(oldestEntry []byte, currentTimestamp uint64, evict func() error) bool {
func (c *BigCache) onEvict(oldestEntry []byte, currentTimestamp uint64, evict func(reason RemoveReason) error) bool {
oldestTimestamp := readTimestampFromEntry(oldestEntry)
if currentTimestamp-oldestTimestamp > c.lifeWindow {
evict()
evict(Expired)
return true
}
return false
Expand All @@ -147,9 +162,15 @@ func (c *BigCache) getShard(hashedKey uint64) (shard *cacheShard) {
return c.shards[hashedKey&c.shardMask]
}

func (c *BigCache) providedOnRemove(wrappedEntry []byte) {
func (c *BigCache) providedOnRemove(wrappedEntry []byte, reason RemoveReason) {
c.config.OnRemove(readKeyFromEntry(wrappedEntry), readEntry(wrappedEntry))
}

func (c *BigCache) notProvidedOnRemove(wrappedEntry []byte) {
func (c *BigCache) providedOnRemoveWithReason(wrappedEntry []byte, reason RemoveReason) {
if c.config.onRemoveFilter == 0 || (1<<uint(reason))&c.config.onRemoveFilter > 0 {
c.config.OnRemoveWithReason(readKeyFromEntry(wrappedEntry), readEntry(wrappedEntry), reason)
}
}

func (c *BigCache) notProvidedOnRemove(wrappedEntry []byte, reason RemoveReason) {
}
69 changes: 69 additions & 0 deletions bigcache_test.go
Expand Up @@ -181,17 +181,52 @@ func TestOnRemoveCallback(t *testing.T) {
// given
clock := mockedClock{value: 0}
onRemoveInvoked := false
onRemoveExtInvoked := false
onRemove := func(key string, entry []byte) {
onRemoveInvoked = true
assert.Equal(t, "key", key)
assert.Equal(t, []byte("value"), entry)
}
onRemoveExt := func(key string, entry []byte, reason RemoveReason) {
onRemoveExtInvoked = true
}
cache, _ := newBigCache(Config{
Shards: 1,
LifeWindow: time.Second,
MaxEntriesInWindow: 1,
MaxEntrySize: 256,
OnRemove: onRemove,
OnRemoveWithReason: onRemoveExt,
}, &clock)

// when
cache.Set("key", []byte("value"))
clock.set(5)
cache.Set("key2", []byte("value2"))

// then
assert.True(t, onRemoveInvoked)
assert.False(t, onRemoveExtInvoked)
}

func TestOnRemoveWithReasonCallback(t *testing.T) {
t.Parallel()

// given
clock := mockedClock{value: 0}
onRemoveInvoked := false
onRemove := func(key string, entry []byte, reason RemoveReason) {
onRemoveInvoked = true
assert.Equal(t, "key", key)
assert.Equal(t, []byte("value"), entry)
assert.Equal(t, reason, RemoveReason(Expired))
}
cache, _ := newBigCache(Config{
Shards: 1,
LifeWindow: time.Second,
MaxEntriesInWindow: 1,
MaxEntrySize: 256,
OnRemoveWithReason: onRemove,
}, &clock)

// when
Expand All @@ -203,6 +238,40 @@ func TestOnRemoveCallback(t *testing.T) {
assert.True(t, onRemoveInvoked)
}

func TestOnRemoveFilter(t *testing.T) {
t.Parallel()

// given
clock := mockedClock{value: 0}
onRemoveInvoked := false
onRemove := func(key string, entry []byte, reason RemoveReason) {
onRemoveInvoked = true
}
c := Config{
Shards: 1,
LifeWindow: time.Second,
MaxEntriesInWindow: 1,
MaxEntrySize: 256,
OnRemoveWithReason: onRemove,
}.OnRemoveFilterSet(Deleted, NoSpace)

cache, _ := newBigCache(c, &clock)

// when
cache.Set("key", []byte("value"))
clock.set(5)
cache.Set("key2", []byte("value2"))

// then
assert.False(t, onRemoveInvoked)

// and when
cache.Delete("key2")

// then
assert.True(t, onRemoveInvoked)
}

func TestCacheLen(t *testing.T) {
t.Parallel()

Expand Down
21 changes: 20 additions & 1 deletion config.go
Expand Up @@ -26,8 +26,16 @@ type Config struct {
// the oldest entries are overridden for the new ones.
HardMaxCacheSize int
// OnRemove is a callback fired when the oldest entry is removed because of its expiration time or no space left
// for the new entry. Default value is nil which means no callback and it prevents from unwrapping the oldest entry.
// for the new entry, or because delete was called.
// Default value is nil which means no callback and it prevents from unwrapping the oldest entry.
OnRemove func(key string, entry []byte)
// OnRemoveWithReason is a callback fired when the oldest entry is removed because of its expiration time or no space left
// for the new entry, or because delete was called. A constant representing the reason will be passed through.
// Default value is nil which means no callback and it prevents from unwrapping the oldest entry.
// Ignored if OnRemove is specified.
OnRemoveWithReason func(key string, entry []byte, reason RemoveReason)

onRemoveFilter int

// Logger is a logging interface and used in combination with `Verbose`
// Defaults to `DefaultLogger()`
Expand Down Expand Up @@ -65,3 +73,14 @@ func (c Config) maximumShardSize() int {

return maxShardSize
}

// OnRemoveFilterSet sets which remove reasons will trigger a call to OnRemoveWithReason.
// Filtering out reasons prevents bigcache from unwrapping them, which saves cpu.
func (c Config) OnRemoveFilterSet(reasons ...RemoveReason) Config {
c.onRemoveFilter = 0
for i := range reasons {
c.onRemoveFilter |= 1 << uint(reasons[i])
}

return c
}
18 changes: 9 additions & 9 deletions shard.go
Expand Up @@ -8,12 +8,14 @@ import (
"github.com/allegro/bigcache/queue"
)

type onRemoveCallback func(wrappedEntry []byte, reason RemoveReason)

type cacheShard struct {
hashmap map[uint64]uint32
entries queue.BytesQueue
lock sync.RWMutex
entryBuffer []byte
onRemove func(wrappedEntry []byte)
onRemove onRemoveCallback

isVerbose bool
logger Logger
Expand All @@ -23,8 +25,6 @@ type cacheShard struct {
stats Stats
}

type onRemoveCallback func(wrappedEntry []byte)

func (s *cacheShard) get(key string, hashedKey uint64) ([]byte, error) {
s.lock.RLock()
itemIndex := s.hashmap[hashedKey]
Expand Down Expand Up @@ -77,7 +77,7 @@ func (s *cacheShard) set(key string, hashedKey uint64, entry []byte) error {
s.lock.Unlock()
return nil
}
if s.removeOldestEntry() != nil {
if s.removeOldestEntry(NoSpace) != nil {
s.lock.Unlock()
return fmt.Errorf("entry is bigger than max shard size")
}
Expand Down Expand Up @@ -105,7 +105,7 @@ func (s *cacheShard) del(key string, hashedKey uint64) error {
s.lock.Lock()
{
delete(s.hashmap, hashedKey)
s.onRemove(wrappedEntry)
s.onRemove(wrappedEntry, Deleted)
resetKeyFromEntry(wrappedEntry)
}
s.lock.Unlock()
Expand All @@ -114,10 +114,10 @@ func (s *cacheShard) del(key string, hashedKey uint64) error {
return nil
}

func (s *cacheShard) onEvict(oldestEntry []byte, currentTimestamp uint64, evict func() error) bool {
func (s *cacheShard) onEvict(oldestEntry []byte, currentTimestamp uint64, evict func(reason RemoveReason) error) bool {
oldestTimestamp := readTimestampFromEntry(oldestEntry)
if currentTimestamp-oldestTimestamp > s.lifeWindow {
evict()
evict(Expired)
return true
}
return false
Expand Down Expand Up @@ -157,12 +157,12 @@ func (s *cacheShard) copyKeys() (keys []uint32, next int) {
return keys, next
}

func (s *cacheShard) removeOldestEntry() error {
func (s *cacheShard) removeOldestEntry(reason RemoveReason) error {
oldest, err := s.entries.Pop()
if err == nil {
hash := readHashFromEntry(oldest)
delete(s.hashmap, hash)
s.onRemove(oldest)
s.onRemove(oldest, reason)
return nil
}
return err
Expand Down

0 comments on commit 0071311

Please sign in to comment.