Skip to content

Commit

Permalink
feat: use atomic.Int64 in Stats to prevent 64bits alignment problems
Browse files Browse the repository at this point in the history
  • Loading branch information
William Petit committed Dec 1, 2023
1 parent 05b47f3 commit 2468f9b
Show file tree
Hide file tree
Showing 5 changed files with 157 additions and 56 deletions.
14 changes: 7 additions & 7 deletions bigcache.go
Original file line number Diff line number Diff line change
Expand Up @@ -209,15 +209,15 @@ func (c *BigCache) Capacity() int {
}

// Stats returns cache's statistics
func (c *BigCache) Stats() Stats {
var s Stats
func (c *BigCache) Stats() *Stats {
s := NewStats(0, 0, 0, 0, 0)
for _, shard := range c.shards {
tmp := shard.getStats()
s.Hits += tmp.Hits
s.Misses += tmp.Misses
s.DelHits += tmp.DelHits
s.DelMisses += tmp.DelMisses
s.Collisions += tmp.Collisions
s.AddHits(tmp.Hits())
s.AddMisses(tmp.Misses())
s.AddDelHits(tmp.DelHits())
s.AddDelMisses(tmp.DelMisses())
s.AddCollisions(tmp.Collisions())
}
return s
}
Expand Down
34 changes: 17 additions & 17 deletions bigcache_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ func TestAppendCollision(t *testing.T) {

// then
noError(t, err)
assertEqual(t, cache.Stats().Collisions, int64(1))
assertEqual(t, cache.Stats().Collisions(), int64(1))
cachedValue, err = cache.Get("b")
noError(t, err)
assertEqual(t, []byte("2"), cachedValue)
Expand Down Expand Up @@ -641,10 +641,10 @@ func TestCacheStats(t *testing.T) {

// then
stats := cache.Stats()
assertEqual(t, stats.Hits, int64(10))
assertEqual(t, stats.Misses, int64(10))
assertEqual(t, stats.DelHits, int64(10))
assertEqual(t, stats.DelMisses, int64(10))
assertEqual(t, stats.Hits(), int64(10))
assertEqual(t, stats.Misses(), int64(10))
assertEqual(t, stats.DelHits(), int64(10))
assertEqual(t, stats.DelMisses(), int64(10))
}
func TestCacheEntryStats(t *testing.T) {
t.Parallel()
Expand Down Expand Up @@ -705,18 +705,18 @@ func TestCacheRestStats(t *testing.T) {
}

stats := cache.Stats()
assertEqual(t, stats.Hits, int64(10))
assertEqual(t, stats.Misses, int64(10))
assertEqual(t, stats.DelHits, int64(10))
assertEqual(t, stats.DelMisses, int64(10))
assertEqual(t, stats.Hits(), int64(10))
assertEqual(t, stats.Misses(), int64(10))
assertEqual(t, stats.DelHits(), int64(10))
assertEqual(t, stats.DelMisses(), int64(10))

//then
cache.ResetStats()
stats = cache.Stats()
assertEqual(t, stats.Hits, int64(0))
assertEqual(t, stats.Misses, int64(0))
assertEqual(t, stats.DelHits, int64(0))
assertEqual(t, stats.DelMisses, int64(0))
assertEqual(t, stats.Hits(), int64(0))
assertEqual(t, stats.Misses(), int64(0))
assertEqual(t, stats.DelHits(), int64(0))
assertEqual(t, stats.DelMisses(), int64(0))
}

func TestCacheDel(t *testing.T) {
Expand Down Expand Up @@ -832,7 +832,7 @@ func TestWriteAndReadParallelSameKeyWithStats(t *testing.T) {

wg.Wait()

assertEqual(t, Stats{Hits: int64(n * ntest)}, cache.Stats())
assertEqual(t, NewStats(int64(n*ntest), 0, 0, 0, 0), cache.Stats())
assertEqual(t, ntest*n, int(cache.KeyMetadata(key).RequestCount))
}

Expand Down Expand Up @@ -1056,7 +1056,7 @@ func TestHashCollision(t *testing.T) {
assertEqual(t, []byte(nil), cachedValue)

assertEqual(t, "Collision detected. Both %q and %q have the same hash %x", ml.lastFormat)
assertEqual(t, cache.Stats().Collisions, int64(1))
assertEqual(t, cache.Stats().Collisions(), int64(1))
}

func TestNilValueCaching(t *testing.T) {
Expand Down Expand Up @@ -1137,7 +1137,7 @@ func TestEntryNotPresent(t *testing.T) {
value, resp, err := cache.GetWithInfo("blah")
assertEqual(t, ErrEntryNotFound, err)
assertEqual(t, resp.EntryStatus, RemoveReason(0))
assertEqual(t, cache.Stats().Misses, int64(1))
assertEqual(t, cache.Stats().Misses(), int64(1))
assertEqual(t, []byte(nil), value)
}

Expand Down Expand Up @@ -1230,7 +1230,7 @@ func TestBigCache_GetWithInfoCollision(t *testing.T) {
assertEqual(t, []byte(nil), cachedValue)
assertEqual(t, Response{}, resp)
assertEqual(t, ErrEntryNotFound, err)
assertEqual(t, cache.Stats().Collisions, int64(1))
assertEqual(t, cache.Stats().Collisions(), int64(1))

}

Expand Down
10 changes: 5 additions & 5 deletions server/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ func TestClearCache(t *testing.T) {
}
func TestGetStats(t *testing.T) {
t.Parallel()
var testStats bigcache.Stats
testStats := bigcache.NewStats(0, 0, 0, 0, 0)

req := httptest.NewRequest("GET", testBaseString+"/api/v1/stats", nil)
rr := httptest.NewRecorder()
Expand All @@ -203,14 +203,14 @@ func TestGetStats(t *testing.T) {
t.Errorf("error decoding cache stats. error: %s", err)
}

if testStats.Hits == 0 {
if testStats.Hits() == 0 {
t.Errorf("want: > 0; got: 0.\n\thandler not properly returning stats info.")
}
}

func TestGetStatsIndex(t *testing.T) {
t.Parallel()
var testStats bigcache.Stats
testStats := bigcache.NewStats(0, 0, 0, 0, 0)

getreq := httptest.NewRequest("GET", testBaseString+"/api/v1/stats", nil)
putreq := httptest.NewRequest("PUT", testBaseString+"/api/v1/stats", nil)
Expand All @@ -229,11 +229,11 @@ func TestGetStatsIndex(t *testing.T) {
testHandlers.ServeHTTP(rr, getreq)
resp := rr.Result()

if err := json.NewDecoder(resp.Body).Decode(&testStats); err != nil {
if err := json.NewDecoder(resp.Body).Decode(testStats); err != nil {
t.Errorf("error decoding cache stats. error: %s", err)
}

if testStats.Hits == 0 {
if testStats.Hits() == 0 {
t.Errorf("want: > 0; got: 0.\n\thandler not properly returning stats info.")
}

Expand Down
29 changes: 11 additions & 18 deletions shard.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package bigcache
import (
"errors"
"sync"
"sync/atomic"

"github.com/allegro/bigcache/v3/queue"
)
Expand All @@ -29,7 +28,7 @@ type cacheShard struct {
lifeWindow uint64

hashmapStats map[uint64]uint32
stats Stats
stats *Stats
cleanEnabled bool
}

Expand Down Expand Up @@ -355,7 +354,7 @@ func (s *cacheShard) reset(config Config) {

func (s *cacheShard) resetStats() {
s.lock.Lock()
s.stats = Stats{}
s.stats = NewStats(0, 0, 0, 0, 0)
s.lock.Unlock()
}

Expand All @@ -373,15 +372,8 @@ func (s *cacheShard) capacity() int {
return res
}

func (s *cacheShard) getStats() Stats {
var stats = Stats{
Hits: atomic.LoadInt64(&s.stats.Hits),
Misses: atomic.LoadInt64(&s.stats.Misses),
DelHits: atomic.LoadInt64(&s.stats.DelHits),
DelMisses: atomic.LoadInt64(&s.stats.DelMisses),
Collisions: atomic.LoadInt64(&s.stats.Collisions),
}
return stats
func (s *cacheShard) getStats() *Stats {
return s.stats.Copy()
}

func (s *cacheShard) getKeyMetadataWithLock(key uint64) Metadata {
Expand All @@ -400,7 +392,7 @@ func (s *cacheShard) getKeyMetadata(key uint64) Metadata {
}

func (s *cacheShard) hit(key uint64) {
atomic.AddInt64(&s.stats.Hits, 1)
s.stats.AddHits(1)
if s.statsEnabled {
s.lock.Lock()
s.hashmapStats[key]++
Expand All @@ -409,26 +401,26 @@ func (s *cacheShard) hit(key uint64) {
}

func (s *cacheShard) hitWithoutLock(key uint64) {
atomic.AddInt64(&s.stats.Hits, 1)
s.stats.AddHits(1)
if s.statsEnabled {
s.hashmapStats[key]++
}
}

func (s *cacheShard) miss() {
atomic.AddInt64(&s.stats.Misses, 1)
s.stats.AddMisses(1)
}

func (s *cacheShard) delhit() {
atomic.AddInt64(&s.stats.DelHits, 1)
s.stats.AddDelHits(1)
}

func (s *cacheShard) delmiss() {
atomic.AddInt64(&s.stats.DelMisses, 1)
s.stats.AddDelMisses(1)
}

func (s *cacheShard) collision() {
atomic.AddInt64(&s.stats.Collisions, 1)
s.stats.AddCollisions(1)
}

func initNewShard(config Config, callback onRemoveCallback, clock clock) *cacheShard {
Expand All @@ -450,5 +442,6 @@ func initNewShard(config Config, callback onRemoveCallback, clock clock) *cacheS
lifeWindow: uint64(config.LifeWindow.Seconds()),
statsEnabled: config.StatsEnabled,
cleanEnabled: config.CleanWindow > 0,
stats: NewStats(0, 0, 0, 0, 0),
}
}
126 changes: 117 additions & 9 deletions stats.go
Original file line number Diff line number Diff line change
@@ -1,15 +1,123 @@
package bigcache

import (
"encoding/json"
"sync/atomic"
)

// Stats stores cache statistics
type Stats struct {
// Hits is a number of successfully found keys
Hits int64 `json:"hits"`
// Misses is a number of not found keys
Misses int64 `json:"misses"`
// DelHits is a number of successfully deleted keys
DelHits int64 `json:"delete_hits"`
// DelMisses is a number of not deleted keys
DelMisses int64 `json:"delete_misses"`
// Collisions is a number of happened key-collisions
hits atomic.Int64
misses atomic.Int64
delHits atomic.Int64
delMisses atomic.Int64
collisions atomic.Int64
}

// Hits returns the number of successfully found keys
func (s *Stats) Hits() int64 {
return s.hits.Load()
}

func (s *Stats) AddHits(delta int64) int64 {
return s.hits.Add(delta)
}

// Misses returns the number of not found keys
func (s *Stats) Misses() int64 {
return s.misses.Load()
}

func (s *Stats) AddMisses(delta int64) int64 {
return s.misses.Add(delta)
}

// DelHits returns the number of successfully deleted keys
func (s *Stats) DelHits() int64 {
return s.delHits.Load()
}

func (s *Stats) AddDelHits(delta int64) int64 {
return s.delHits.Add(delta)
}

// DelMisses returns the number of not deleted keys
func (s *Stats) DelMisses() int64 {
return s.delMisses.Load()
}

func (s *Stats) AddDelMisses(delta int64) int64 {
return s.delMisses.Add(delta)
}

// Collisions returns the number of happened key-collisions
func (s *Stats) Collisions() int64 {
return s.collisions.Load()
}

func (s *Stats) AddCollisions(delta int64) int64 {
return s.collisions.Add(delta)
}

func (s *Stats) Copy() *Stats {
return NewStats(
s.Hits(),
s.Misses(),
s.DelHits(),
s.DelMisses(),
s.Collisions(),
)
}

func NewStats(hits, misses, delHits, delMisses, collisions int64) *Stats {
stats := &Stats{
hits: atomic.Int64{},
misses: atomic.Int64{},
delHits: atomic.Int64{},
delMisses: atomic.Int64{},
collisions: atomic.Int64{},
}

stats.hits.Store(hits)
stats.misses.Store(misses)
stats.delHits.Store(delHits)
stats.delMisses.Store(delMisses)
stats.collisions.Store(collisions)

return stats
}

type jsonStats struct {
Hits int64 `json:"hits"`
Misses int64 `json:"misses"`
DelHits int64 `json:"delete_hits"`
DelMisses int64 `json:"delete_misses"`
Collisions int64 `json:"collisions"`
}

func (s *Stats) MarshalJSON() ([]byte, error) {
jsonStats := jsonStats{
Hits: s.hits.Load(),
Misses: s.misses.Load(),
DelHits: s.delHits.Load(),
DelMisses: s.delMisses.Load(),
Collisions: s.collisions.Load(),
}

return json.Marshal(jsonStats)
}

func (s *Stats) UnmarshalJSON(data []byte) error {
jsonStats := jsonStats{}
if err := json.Unmarshal(data, &jsonStats); err != nil {
return err
}

s.hits.Store(jsonStats.Hits)
s.misses.Store(jsonStats.Misses)
s.delHits.Store(jsonStats.DelHits)
s.delMisses.Store(jsonStats.DelMisses)
s.collisions.Store(jsonStats.Collisions)

return nil
}

0 comments on commit 2468f9b

Please sign in to comment.