Repository: allegro/bigcache
Pull request #168 (2019-09-23 23:27:15)

New Get API which returns additional information about the entry
enhancement: added new data struct Response, which will be returned when
calling GetWithInfo(). Caller can use this Response to check the status
of the Entry, whether its Expired or not. Default '0' means its still
in use.

minor lint changes

Test: added test case for the api and made sure all tests cases are
successful

coveralls: 
[![Coverage Status](https://coveralls.io/builds/25903285/badge)](https://coveralls.io/builds/25903285)

Coverage decreased (-1.3%) to 89.4% when pulling **f76359e4b1b989dc01f144a5baf225164608eea8 on jgheewala:get_api_info** into **2d6fab29e9cd1d753a47f07e33126065067d6152 on allegro:master**.

jgheewala: @mxplusb I forgot to `add` my test cases. Sorry about that. Have added it now.

Patch:
diff --git a/bigcache.go b/bigcache.go
index b387926..bd205ce 100644
--- a/bigcache.go
+++ b/bigcache.go
@@ -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
@@ -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)
@@ -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
diff --git a/entry_not_found_error.go b/entry_not_found_error.go
index 051a712..4a5b5f9 100644
--- a/entry_not_found_error.go
+++ b/entry_not_found_error.go
@@ -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")
+)
diff --git a/shard.go b/shard.go
index 1684df8..36a6d9d 100644
--- a/shard.go
+++ b/shard.go
@@ -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]
 
@@ -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 {
@@ -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]


Test patch:
diff --git a/bigcache_bench_test.go b/bigcache_bench_test.go
index 75c19f5..ee4adc2 100644
--- a/bigcache_bench_test.go
+++ b/bigcache_bench_test.go
@@ -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)
@@ -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,
@@ -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)))
+			}
 		}
 	})
 }
diff --git a/bigcache_test.go b/bigcache_test.go
index 580777d..4d5e2cb 100644
--- a/bigcache_test.go
+++ b/bigcache_test.go
@@ -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()
 
@@ -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{}