Skip to content

New Get API which returns additional information about the entry #168

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Oct 1, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion bigcache.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,16 @@ type BigCache struct {
close chan struct{}
}

// Response will contain metadata about the entry for which GetWithInfo(key) was called
type Response struct {
EntryStatus RemoveReason
}

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

const (
// @TODO: Go defaults to 0 so in case we want to return EntryStatus back to the caller Expired cannot be differentiated
// 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
Expand Down Expand Up @@ -110,6 +116,12 @@ func (c *BigCache) Get(key string) ([]byte, error) {
return shard.get(key, hashedKey)
}

func (c *BigCache) GetWithInfo(key string) ([]byte, Response, error) {
hashedKey := c.hash.Sum64(key)
shard := c.getShard(hashedKey)
return shard.getWithInfo(key, hashedKey)
}

// Set saves entry under the key
func (c *BigCache) Set(key string, entry []byte) error {
hashedKey := c.hash.Sum64(key)
Expand All @@ -121,7 +133,7 @@ func (c *BigCache) Set(key string, entry []byte) error {
func (c *BigCache) Delete(key string) error {
hashedKey := c.hash.Sum64(key)
shard := c.getShard(hashedKey)
return shard.del(key, hashedKey)
return shard.del(hashedKey)
}

// Reset empties all cache shards
Expand Down
17 changes: 14 additions & 3 deletions bigcache_bench_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,18 @@ func BenchmarkWriteToCache(b *testing.B) {
func BenchmarkReadFromCache(b *testing.B) {
for _, shards := range []int{1, 512, 1024, 8192} {
b.Run(fmt.Sprintf("%d-shards", shards), func(b *testing.B) {
readFromCache(b, 1024)
readFromCache(b, 1024, false)
})
}
}

func BenchmarkReadFromCacheWithInfo(b *testing.B) {
for _, shards := range []int{1, 512, 1024, 8192} {
b.Run(fmt.Sprintf("%d-shards", shards), func(b *testing.B) {
readFromCache(b, 1024, true)
})
}
}
func BenchmarkIterateOverCache(b *testing.B) {

m := blob('a', 1)
Expand Down Expand Up @@ -127,7 +134,7 @@ func writeToCache(b *testing.B, shards int, lifeWindow time.Duration, requestsIn
})
}

func readFromCache(b *testing.B, shards int) {
func readFromCache(b *testing.B, shards int, info bool) {
cache, _ := NewBigCache(Config{
Shards: shards,
LifeWindow: 1000 * time.Second,
Expand All @@ -143,7 +150,11 @@ func readFromCache(b *testing.B, shards int) {
b.ReportAllocs()

for pb.Next() {
cache.Get(strconv.Itoa(rand.Intn(b.N)))
if info {
cache.GetWithInfo(strconv.Itoa(rand.Intn(b.N)))
} else {
cache.Get(strconv.Itoa(rand.Intn(b.N)))
}
}
})
}
Expand Down
73 changes: 73 additions & 0 deletions bigcache_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,28 @@ func TestCacheLen(t *testing.T) {
assert.Equal(t, keys, cache.Len())
}

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

// given
cache, _ := NewBigCache(Config{
Shards: 8,
LifeWindow: time.Second,
MaxEntriesInWindow: 1,
MaxEntrySize: 256,
})
keys := 1337

// when
for i := 0; i < keys; i++ {
cache.Set(fmt.Sprintf("key%d", i), []byte("value"))
}

// then
assert.Equal(t, keys, cache.Len())
assert.Equal(t, 81920, cache.Capacity())
}

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

Expand Down Expand Up @@ -703,6 +725,57 @@ func TestClosing(t *testing.T) {
assert.InDelta(t, endGR, startGR, 25)
}

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

// given
clock := mockedClock{value: 0}
cache, _ := newBigCache(Config{
Shards: 1,
LifeWindow: 5 * time.Second,
MaxEntriesInWindow: 1,
MaxEntrySize: 1,
HardMaxCacheSize: 1,
}, &clock)

// when
value, resp, err := cache.GetWithInfo("blah")
assert.Error(t, err)
assert.Equal(t, resp.EntryStatus, RemoveReason(0))
assert.Equal(t, cache.Stats().Misses, int64(1))
assert.Nil(t, value)
}

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

// given
clock := mockedClock{value: 0}
cache, _ := newBigCache(Config{
Shards: 1,
LifeWindow: 5 * time.Second,
CleanWindow: 5 * time.Minute,
MaxEntriesInWindow: 1,
MaxEntrySize: 1,
HardMaxCacheSize: 1,
Verbose: true,
}, &clock)
key := "deadEntryKey"
value := "100"
cache.Set(key, []byte(value))

// when
data, resp, err := cache.GetWithInfo(key)
assert.Equal(t, []byte(value), data)
assert.NoError(t, err)
assert.Equal(t, Response{}, resp)
clock.set(5)
data, resp, err = cache.GetWithInfo(key)
assert.Equal(t, err, ErrEntryIsDead)
assert.Equal(t, Response{EntryStatus: Expired}, resp)
assert.Nil(t, data)
}

type mockedLogger struct {
lastFormat string
lastArgs []interface{}
Expand Down
8 changes: 6 additions & 2 deletions entry_not_found_error.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,9 @@ package bigcache

import "errors"

// ErrEntryNotFound is an error type struct which is returned when entry was not found for provided key
var ErrEntryNotFound = errors.New("Entry not found")
var (
// ErrEntryNotFound is an error type struct which is returned when entry was not found for provided key
ErrEntryNotFound = errors.New("Entry not found")
// ErrEntryIsDead is an error type struct which is returned when entry has past it's life window
ErrEntryIsDead = errors.New("entry is dead")
)
69 changes: 55 additions & 14 deletions shard.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,58 @@ type cacheShard struct {
stats Stats
}

func (s *cacheShard) getWithInfo(key string, hashedKey uint64) (entry []byte, resp Response, err error) {
currentTime := uint64(s.clock.epoch())
wrappedEntry, err := s.getWrappedEntry(key, hashedKey)
if err == nil {
s.lock.RLock()
if entryKey := readKeyFromEntry(wrappedEntry); key != entryKey {
if s.isVerbose {
s.logger.Printf("Collision detected. Both %q and %q have the same hash %x", key, entryKey, hashedKey)
}
s.lock.RUnlock()
s.collision()
return entry, resp, ErrEntryNotFound
}

oldestTimeStamp := readTimestampFromEntry(wrappedEntry)
if currentTime-oldestTimeStamp >= s.lifeWindow {
s.lock.RUnlock()
// @TODO: when Expired is non-default value return err as nil as the resp will have proper entry status
resp.EntryStatus = Expired
return entry, resp, ErrEntryIsDead
}
entry := readEntry(wrappedEntry)
s.lock.RUnlock()
s.hit()
return entry, resp, nil
}
// it is nil & error
return wrappedEntry, resp, err
}

func (s *cacheShard) get(key string, hashedKey uint64) ([]byte, error) {
wrappedEntry, err := s.getWrappedEntry(key, hashedKey)
if err == nil {
s.lock.RLock()
if entryKey := readKeyFromEntry(wrappedEntry); key != entryKey {
if s.isVerbose {
s.logger.Printf("Collision detected. Both %q and %q have the same hash %x", key, entryKey, hashedKey)
}
s.lock.RUnlock()
s.collision()
return nil, ErrEntryNotFound
}
entry := readEntry(wrappedEntry)
s.lock.RUnlock()
s.hit()
return entry, nil
}
// it is nil & error
return wrappedEntry, err
}

func (s *cacheShard) getWrappedEntry(key string, hashedKey uint64) ([]byte, error) {
s.lock.RLock()
itemIndex := s.hashmap[hashedKey]

Expand All @@ -36,23 +87,13 @@ func (s *cacheShard) get(key string, hashedKey uint64) ([]byte, error) {
}

wrappedEntry, err := s.entries.Get(int(itemIndex))
s.lock.RUnlock()
if err != nil {
s.lock.RUnlock()
s.miss()
return nil, err
}
if entryKey := readKeyFromEntry(wrappedEntry); key != entryKey {
if s.isVerbose {
s.logger.Printf("Collision detected. Both %q and %q have the same hash %x", key, entryKey, hashedKey)
}
s.lock.RUnlock()
s.collision()
return nil, ErrEntryNotFound
}
entry := readEntry(wrappedEntry)
s.lock.RUnlock()
s.hit()
return entry, nil

return wrappedEntry, err
}

func (s *cacheShard) set(key string, hashedKey uint64, entry []byte) error {
Expand Down Expand Up @@ -85,7 +126,7 @@ func (s *cacheShard) set(key string, hashedKey uint64, entry []byte) error {
}
}

func (s *cacheShard) del(key string, hashedKey uint64) error {
func (s *cacheShard) del(hashedKey uint64) error {
// Optimistic pre-check using only readlock
s.lock.RLock()
itemIndex := s.hashmap[hashedKey]
Expand Down