From ee14c423a7f9ef4c109ce85384df74126cdd72f3 Mon Sep 17 00:00:00 2001 From: raeidish Date: Sun, 15 Oct 2023 12:38:42 +0200 Subject: [PATCH 01/16] first iteration --- bigcache.go | 45 +++++++++++++++++++++++++++++++ bigcache_bench_test.go | 61 ++++++++++++++++++++++++++++++++++++++++++ shard.go | 31 ++++++++++++++++----- 3 files changed, 131 insertions(+), 6 deletions(-) diff --git a/bigcache.go b/bigcache.go index 5620c0ef..aa4192f0 100644 --- a/bigcache.go +++ b/bigcache.go @@ -137,6 +137,51 @@ func (c *BigCache) Get(key string) ([]byte, error) { return shard.get(key, hashedKey) } +// GetMulti reads entry for each of the keys. +// It returns an ErrEntryNotFound when +// no entry exists for the given key. +func (c *BigCache) GetMulti(keys []string) ([][]byte, error) { + + entries := make([][]byte,len(keys)) + + //keeps response ordered + order := make(map[string]int) + + shardMap := make(map[uint64]map[string]uint64) + + for i,k := range keys{ + hk := c.hash.Sum64(k) + shardKey := hk&c.shardMask + + if shardMap[shardKey] == nil { + shardMap[shardKey] = make(map[string]uint64) + } + + shardMap[hk&c.shardMask][k] = hk + order[k] = i + } + + for sI,khash := range shardMap{ + shard := c.shards[sI] + shard.lock.RLock() + for key,keyhash := range khash{ + entry,err := shard.getWithoutLock(key,keyhash) + + if err != nil{ + shard.lock.RUnlock() + return nil,err + } + + entries[order[key]] = entry + + } + shard.lock.RUnlock() + } + + return entries,nil + +} + // GetWithInfo reads entry for the key with Response info. // It returns an ErrEntryNotFound when // no entry exists for the given key. diff --git a/bigcache_bench_test.go b/bigcache_bench_test.go index b6d044e5..c2cf4457 100644 --- a/bigcache_bench_test.go +++ b/bigcache_bench_test.go @@ -69,6 +69,67 @@ func BenchmarkReadFromCache(b *testing.B) { } } +func BenchmarkReadFromCacheManySingle(b *testing.B) { + for _, shards := range []int{1, 512, 1024, 8192} { + b.Run(fmt.Sprintf("%d-shards", shards), func(b *testing.B) { + cache, _ := New(context.Background(), Config{ + Shards: shards, + LifeWindow: 1000 * time.Second, + MaxEntriesInWindow: max(b.N, 100), + MaxEntrySize: 500, + }) + + keys := make([]string,b.N) + for i := 0; i < b.N; i++ { + keys[i] = fmt.Sprintf("key-%d", i) + cache.Set(keys[i], message) + } + + b.ReportAllocs() + b.ResetTimer() + for _,key := range keys { + cache.Get(key) + } + + }) + } +} + + +func BenchmarkReadFromCacheManyMulti(b *testing.B) { + for _, shards := range []int{1, 512, 1024, 8192} { + for _, batchSize := range []int{1, 5, 10, 100} { + b.Run(fmt.Sprintf("%d-shards %d-batchSize", shards,batchSize), func(b *testing.B) { + cache, _ := New(context.Background(), Config{ + Shards: shards, + LifeWindow: 1000 * time.Second, + MaxEntriesInWindow: max(b.N, 100), + MaxEntrySize: 500, + }) + keys := make([]string,b.N) + for i := 0; i < b.N; i++ { + keys[i] = fmt.Sprintf("key-%d", i) + cache.Set(keys[i], message) + } + + batches := make([][]string, 0, (len(keys) + batchSize - 1) / batchSize) + + for batchSize < len(keys) { + keys, batches = keys[batchSize:], append(batches, keys[0:batchSize:batchSize]) + } + batches = append(batches, keys) + + b.ReportAllocs() + b.ResetTimer() + for _,b := range batches{ + cache.GetMulti(b) + + } + }) + } + } +} + func BenchmarkReadFromCacheWithInfo(b *testing.B) { for _, shards := range []int{1, 512, 1024, 8192} { b.Run(fmt.Sprintf("%d-shards", shards), func(b *testing.B) { diff --git a/shard.go b/shard.go index f4d2400b..2de86fc3 100644 --- a/shard.go +++ b/shard.go @@ -16,7 +16,7 @@ type Metadata struct { } type cacheShard struct { - hashmap map[uint64]uint32 + hashmap map[uint64]uint64 entries queue.BytesQueue lock sync.RWMutex entryBuffer []byte @@ -81,6 +81,25 @@ func (s *cacheShard) get(key string, hashedKey uint64) ([]byte, error) { return entry, nil } +func (s *cacheShard) getWithoutLock(key string, hashedKey uint64) ([]byte, error) { + wrappedEntry, err := s.getWrappedEntry(hashedKey) + if err != nil { + return nil, err + } + + if entryKey := readKeyFromEntry(wrappedEntry); key != entryKey { + s.collision() + if s.isVerbose { + s.logger.Printf("Collision detected. Both %q and %q have the same hash %x", key, entryKey, hashedKey) + } + return nil, ErrEntryNotFound + } + entry := readEntry(wrappedEntry) + s.hitWithoutLock(hashedKey) + + return entry, nil +} + func (s *cacheShard) getWrappedEntry(hashedKey uint64) ([]byte, error) { itemIndex := s.hashmap[hashedKey] @@ -140,7 +159,7 @@ func (s *cacheShard) set(key string, hashedKey uint64, entry []byte) error { for { if index, err := s.entries.Push(w); err == nil { - s.hashmap[hashedKey] = uint32(index) + s.hashmap[hashedKey] = uint64(index) s.lock.Unlock() return nil } @@ -164,7 +183,7 @@ func (s *cacheShard) addNewWithoutLock(key string, hashedKey uint64, entry []byt for { if index, err := s.entries.Push(w); err == nil { - s.hashmap[hashedKey] = uint32(index) + s.hashmap[hashedKey] = uint64(index) return nil } if s.removeOldestEntry(NoSpace) != nil { @@ -188,7 +207,7 @@ func (s *cacheShard) setWrappedEntryWithoutLock(currentTimestamp uint64, w []byt for { if index, err := s.entries.Push(w); err == nil { - s.hashmap[hashedKey] = uint32(index) + s.hashmap[hashedKey] = uint64(index) return nil } if s.removeOldestEntry(NoSpace) != nil { @@ -347,7 +366,7 @@ func (s *cacheShard) removeOldestEntry(reason RemoveReason) error { func (s *cacheShard) reset(config Config) { s.lock.Lock() - s.hashmap = make(map[uint64]uint32, config.initialShardSize()) + s.hashmap = make(map[uint64]uint64, config.initialShardSize()) s.entryBuffer = make([]byte, config.MaxEntrySize+headersSizeInBytes) s.entries.Reset() s.lock.Unlock() @@ -438,7 +457,7 @@ func initNewShard(config Config, callback onRemoveCallback, clock clock) *cacheS bytesQueueInitialCapacity = maximumShardSizeInBytes } return &cacheShard{ - hashmap: make(map[uint64]uint32, config.initialShardSize()), + hashmap: make(map[uint64]uint64, config.initialShardSize()), hashmapStats: make(map[uint64]uint32, config.initialShardSize()), entries: *queue.NewBytesQueue(bytesQueueInitialCapacity, maximumShardSizeInBytes, config.Verbose), entryBuffer: make([]byte, config.MaxEntrySize+headersSizeInBytes), From cbff637f10c10c1cca0c21808656b08615ad2c20 Mon Sep 17 00:00:00 2001 From: raeidish Date: Sun, 15 Oct 2023 13:48:28 +0200 Subject: [PATCH 02/16] second itteration --- bigcache.go | 39 ++++++++++----------------------------- bigcache_bench_test.go | 24 +++++++++++++++++++++++- bigcache_test.go | 22 ++++++++++++++++++++++ 3 files changed, 55 insertions(+), 30 deletions(-) diff --git a/bigcache.go b/bigcache.go index aa4192f0..bcf45c46 100644 --- a/bigcache.go +++ b/bigcache.go @@ -143,43 +143,24 @@ func (c *BigCache) Get(key string) ([]byte, error) { func (c *BigCache) GetMulti(keys []string) ([][]byte, error) { entries := make([][]byte,len(keys)) + var err error + shardsLocked := make(map[uint64]bool) - //keeps response ordered - order := make(map[string]int) - - shardMap := make(map[uint64]map[string]uint64) - - for i,k := range keys{ - hk := c.hash.Sum64(k) + for i := range keys{ + hk := c.hash.Sum64(keys[i]) shardKey := hk&c.shardMask - if shardMap[shardKey] == nil { - shardMap[shardKey] = make(map[string]uint64) + if !shardsLocked[shardKey]{ + c.shards[shardKey].lock.RLock() + defer c.shards[shardKey].lock.RUnlock() } - shardMap[hk&c.shardMask][k] = hk - order[k] = i - } - - for sI,khash := range shardMap{ - shard := c.shards[sI] - shard.lock.RLock() - for key,keyhash := range khash{ - entry,err := shard.getWithoutLock(key,keyhash) - - if err != nil{ - shard.lock.RUnlock() - return nil,err - } - - entries[order[key]] = entry - + entries[i],err = c.shards[shardKey].getWithoutLock(keys[i],hk) + if err != nil{ + return nil,err } - shard.lock.RUnlock() } - return entries,nil - } // GetWithInfo reads entry for the key with Response info. diff --git a/bigcache_bench_test.go b/bigcache_bench_test.go index c2cf4457..51c7d6ab 100644 --- a/bigcache_bench_test.go +++ b/bigcache_bench_test.go @@ -95,8 +95,30 @@ func BenchmarkReadFromCacheManySingle(b *testing.B) { } } - func BenchmarkReadFromCacheManyMulti(b *testing.B) { + for _, shards := range []int{1, 512, 1024, 8192} { + b.Run(fmt.Sprintf("%d-shards", shards), func(b *testing.B) { + cache, _ := New(context.Background(), Config{ + Shards: shards, + LifeWindow: 1000 * time.Second, + MaxEntriesInWindow: max(b.N, 100), + MaxEntrySize: 500, + }) + keys := make([]string,b.N) + for i := 0; i < b.N; i++ { + keys[i] = fmt.Sprintf("key-%d", i) + cache.Set(keys[i], message) + } + + b.ReportAllocs() + b.ResetTimer() + cache.GetMulti(keys) + }) + } +} + + +func BenchmarkReadFromCacheManyMultiBatches(b *testing.B) { for _, shards := range []int{1, 512, 1024, 8192} { for _, batchSize := range []int{1, 5, 10, 100} { b.Run(fmt.Sprintf("%d-shards %d-batchSize", shards,batchSize), func(b *testing.B) { diff --git a/bigcache_test.go b/bigcache_test.go index 43ec3f57..4de240c7 100644 --- a/bigcache_test.go +++ b/bigcache_test.go @@ -29,6 +29,28 @@ func TestWriteAndGetOnCache(t *testing.T) { assertEqual(t, value, cachedValue) } +func TestWriteAndGetOnCacheMulti(t *testing.T) { + t.Parallel() + + // given + cache, _ := New(context.Background(), DefaultConfig(5*time.Second)) + keys := []string{"k1","k2","k3","k4","k5"} + values := [][]byte{[]byte("v1"),[]byte("v2"),[]byte("v3"),[]byte("v4"),[]byte("v5")} + + // when + for i,key := range keys{ + cache.Set(key,values[i]) + } + cachedValues, err := cache.GetMulti(keys) + + // then + noError(t, err) + + for i,cachedValue := range cachedValues{ + assertEqual(t,values[i],cachedValue) + } +} + func TestAppendAndGetOnCache(t *testing.T) { t.Parallel() From b18b7509ba4bf422942b7a36fc0e81eb392480c1 Mon Sep 17 00:00:00 2001 From: raeidish Date: Sun, 15 Oct 2023 14:26:08 +0200 Subject: [PATCH 03/16] change to improve readability --- bigcache.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/bigcache.go b/bigcache.go index bcf45c46..9960ab1e 100644 --- a/bigcache.go +++ b/bigcache.go @@ -147,15 +147,15 @@ func (c *BigCache) GetMulti(keys []string) ([][]byte, error) { shardsLocked := make(map[uint64]bool) for i := range keys{ - hk := c.hash.Sum64(keys[i]) - shardKey := hk&c.shardMask + hashedKey := c.hash.Sum64(keys[i]) + shard := c.getShard(hashedKey) - if !shardsLocked[shardKey]{ - c.shards[shardKey].lock.RLock() - defer c.shards[shardKey].lock.RUnlock() + if !shardsLocked[hashedKey & c.shardMask]{ + shard.lock.RLock() + defer shard.lock.RUnlock() } - entries[i],err = c.shards[shardKey].getWithoutLock(keys[i],hk) + entries[i],err = shard.getWithoutLock(keys[i],hashedKey) if err != nil{ return nil,err } From 8ea6ee26b5c6fe8f35aa2b4f451f75b1c9344381 Mon Sep 17 00:00:00 2001 From: raeidish Date: Sun, 15 Oct 2023 17:19:11 +0200 Subject: [PATCH 04/16] fix add shard to map --- bigcache.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bigcache.go b/bigcache.go index 9960ab1e..6aa64fdf 100644 --- a/bigcache.go +++ b/bigcache.go @@ -152,6 +152,7 @@ func (c *BigCache) GetMulti(keys []string) ([][]byte, error) { if !shardsLocked[hashedKey & c.shardMask]{ shard.lock.RLock() + shardsLocked[hashedKey & c.shardMask] = true defer shard.lock.RUnlock() } @@ -160,6 +161,7 @@ func (c *BigCache) GetMulti(keys []string) ([][]byte, error) { return nil,err } } + return entries,nil } From 799d56e607c9e0b7540b861b634905fe2253d0ff Mon Sep 17 00:00:00 2001 From: raeidish Date: Tue, 17 Oct 2023 00:39:43 +0200 Subject: [PATCH 05/16] wip --- shard.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/shard.go b/shard.go index 2de86fc3..1bf47fa0 100644 --- a/shard.go +++ b/shard.go @@ -16,7 +16,7 @@ type Metadata struct { } type cacheShard struct { - hashmap map[uint64]uint64 + hashmap map[uint64]uint32 entries queue.BytesQueue lock sync.RWMutex entryBuffer []byte @@ -159,7 +159,7 @@ func (s *cacheShard) set(key string, hashedKey uint64, entry []byte) error { for { if index, err := s.entries.Push(w); err == nil { - s.hashmap[hashedKey] = uint64(index) + s.hashmap[hashedKey] = uint32(index) s.lock.Unlock() return nil } @@ -183,7 +183,7 @@ func (s *cacheShard) addNewWithoutLock(key string, hashedKey uint64, entry []byt for { if index, err := s.entries.Push(w); err == nil { - s.hashmap[hashedKey] = uint64(index) + s.hashmap[hashedKey] = uint32(index) return nil } if s.removeOldestEntry(NoSpace) != nil { @@ -207,7 +207,7 @@ func (s *cacheShard) setWrappedEntryWithoutLock(currentTimestamp uint64, w []byt for { if index, err := s.entries.Push(w); err == nil { - s.hashmap[hashedKey] = uint64(index) + s.hashmap[hashedKey] = uint32(index) return nil } if s.removeOldestEntry(NoSpace) != nil { @@ -366,7 +366,7 @@ func (s *cacheShard) removeOldestEntry(reason RemoveReason) error { func (s *cacheShard) reset(config Config) { s.lock.Lock() - s.hashmap = make(map[uint64]uint64, config.initialShardSize()) + s.hashmap = make(map[uint64]uint32, config.initialShardSize()) s.entryBuffer = make([]byte, config.MaxEntrySize+headersSizeInBytes) s.entries.Reset() s.lock.Unlock() @@ -457,7 +457,7 @@ func initNewShard(config Config, callback onRemoveCallback, clock clock) *cacheS bytesQueueInitialCapacity = maximumShardSizeInBytes } return &cacheShard{ - hashmap: make(map[uint64]uint64, config.initialShardSize()), + hashmap: make(map[uint64]uint32, config.initialShardSize()), hashmapStats: make(map[uint64]uint32, config.initialShardSize()), entries: *queue.NewBytesQueue(bytesQueueInitialCapacity, maximumShardSizeInBytes, config.Verbose), entryBuffer: make([]byte, config.MaxEntrySize+headersSizeInBytes), From bd294c27fafa8810490e5ae8ed76e54f54266441 Mon Sep 17 00:00:00 2001 From: raeidish Date: Tue, 17 Oct 2023 00:41:22 +0200 Subject: [PATCH 06/16] wip precompute shards for all keys --- bigcache.go | 49 +++++++++++------- bigcache_bench_test.go | 113 ++++++++++++++++++++--------------------- bigcache_test.go | 16 +++--- 3 files changed, 95 insertions(+), 83 deletions(-) diff --git a/bigcache.go b/bigcache.go index 6aa64fdf..e8e0d8a9 100644 --- a/bigcache.go +++ b/bigcache.go @@ -137,32 +137,45 @@ func (c *BigCache) Get(key string) ([]byte, error) { return shard.get(key, hashedKey) } +// Used to sore information about keys in GetMulti function +// order is the index in the slice the data should go +// hashedKey is the Sum64 hash of the key +// key is the original key input +type keyInfo struct { + order int + hashedKey uint64 + key string +} + // GetMulti reads entry for each of the keys. // It returns an ErrEntryNotFound when // no entry exists for the given key. func (c *BigCache) GetMulti(keys []string) ([][]byte, error) { + shards := make(map[uint64][]keyInfo) + entries := make([][]byte, len(keys)) - entries := make([][]byte,len(keys)) - var err error - shardsLocked := make(map[uint64]bool) - - for i := range keys{ - hashedKey := c.hash.Sum64(keys[i]) - shard := c.getShard(hashedKey) + for i, key := range keys { + hashedKey := c.hash.Sum64(key) + shardIndex := hashedKey & c.shardMask + shards[shardIndex] = append(shards[shardIndex], keyInfo{order: i, hashedKey: hashedKey, key: key}) + } - if !shardsLocked[hashedKey & c.shardMask]{ - shard.lock.RLock() - shardsLocked[hashedKey & c.shardMask] = true - defer shard.lock.RUnlock() - } + for shardKey, keyInfos := range shards { + shard := c.shards[shardKey] + shard.lock.RLock() - entries[i],err = shard.getWithoutLock(keys[i],hashedKey) - if err != nil{ - return nil,err - } - } + for i := range keyInfos { + entry, err := shard.getWithoutLock(keyInfos[i].key, keyInfos[i].hashedKey) - return entries,nil + if err != nil { + shard.lock.RUnlock() + return nil, err + } + entries[keyInfos[i].order] = entry + } + shard.lock.RUnlock() + } + return entries, nil } // GetWithInfo reads entry for the key with Response info. diff --git a/bigcache_bench_test.go b/bigcache_bench_test.go index 51c7d6ab..a4be6477 100644 --- a/bigcache_bench_test.go +++ b/bigcache_bench_test.go @@ -70,7 +70,7 @@ func BenchmarkReadFromCache(b *testing.B) { } func BenchmarkReadFromCacheManySingle(b *testing.B) { - for _, shards := range []int{1, 512, 1024, 8192} { + for _, shards := range []int{1, 512, 1024, 8192} { b.Run(fmt.Sprintf("%d-shards", shards), func(b *testing.B) { cache, _ := New(context.Background(), Config{ Shards: shards, @@ -79,17 +79,17 @@ func BenchmarkReadFromCacheManySingle(b *testing.B) { MaxEntrySize: 500, }) - keys := make([]string,b.N) + keys := make([]string, b.N) for i := 0; i < b.N; i++ { - keys[i] = fmt.Sprintf("key-%d", i) + keys[i] = fmt.Sprintf("key-%d", i) cache.Set(keys[i], message) } - b.ReportAllocs() - b.ResetTimer() - for _,key := range keys { - cache.Get(key) - } + b.ReportAllocs() + b.ResetTimer() + for _, key := range keys { + cache.Get(key) + } }) } @@ -97,59 +97,58 @@ func BenchmarkReadFromCacheManySingle(b *testing.B) { func BenchmarkReadFromCacheManyMulti(b *testing.B) { for _, shards := range []int{1, 512, 1024, 8192} { - b.Run(fmt.Sprintf("%d-shards", shards), func(b *testing.B) { - cache, _ := New(context.Background(), Config{ - Shards: shards, - LifeWindow: 1000 * time.Second, - MaxEntriesInWindow: max(b.N, 100), - MaxEntrySize: 500, - }) - keys := make([]string,b.N) - for i := 0; i < b.N; i++ { - keys[i] = fmt.Sprintf("key-%d", i) - cache.Set(keys[i], message) - } - - b.ReportAllocs() - b.ResetTimer() - cache.GetMulti(keys) - }) - } -} + b.Run(fmt.Sprintf("%d-shards", shards), func(b *testing.B) { + cache, _ := New(context.Background(), Config{ + Shards: shards, + LifeWindow: 1000 * time.Second, + MaxEntriesInWindow: max(b.N, 100), + MaxEntrySize: 500, + }) + keys := make([]string, b.N) + for i := 0; i < b.N; i++ { + keys[i] = fmt.Sprintf("key-%d", i) + cache.Set(keys[i], message) + } + b.ReportAllocs() + b.ResetTimer() + cache.GetMulti(keys) + }) + } +} func BenchmarkReadFromCacheManyMultiBatches(b *testing.B) { for _, shards := range []int{1, 512, 1024, 8192} { - for _, batchSize := range []int{1, 5, 10, 100} { - b.Run(fmt.Sprintf("%d-shards %d-batchSize", shards,batchSize), func(b *testing.B) { - cache, _ := New(context.Background(), Config{ - Shards: shards, - LifeWindow: 1000 * time.Second, - MaxEntriesInWindow: max(b.N, 100), - MaxEntrySize: 500, - }) - keys := make([]string,b.N) - for i := 0; i < b.N; i++ { - keys[i] = fmt.Sprintf("key-%d", i) - cache.Set(keys[i], message) - } - - batches := make([][]string, 0, (len(keys) + batchSize - 1) / batchSize) - - for batchSize < len(keys) { - keys, batches = keys[batchSize:], append(batches, keys[0:batchSize:batchSize]) - } - batches = append(batches, keys) - - b.ReportAllocs() - b.ResetTimer() - for _,b := range batches{ - cache.GetMulti(b) - - } - }) - } - } + for _, batchSize := range []int{1, 5, 10, 100} { + b.Run(fmt.Sprintf("%d-shards %d-batchSize", shards, batchSize), func(b *testing.B) { + cache, _ := New(context.Background(), Config{ + Shards: shards, + LifeWindow: 1000 * time.Second, + MaxEntriesInWindow: max(b.N, 100), + MaxEntrySize: 500, + }) + keys := make([]string, b.N) + for i := 0; i < b.N; i++ { + keys[i] = fmt.Sprintf("key-%d", i) + cache.Set(keys[i], message) + } + + batches := make([][]string, 0, (len(keys)+batchSize-1)/batchSize) + + for batchSize < len(keys) { + keys, batches = keys[batchSize:], append(batches, keys[0:batchSize:batchSize]) + } + batches = append(batches, keys) + + b.ReportAllocs() + b.ResetTimer() + for _, b := range batches { + cache.GetMulti(b) + + } + }) + } + } } func BenchmarkReadFromCacheWithInfo(b *testing.B) { diff --git a/bigcache_test.go b/bigcache_test.go index 4de240c7..16487caf 100644 --- a/bigcache_test.go +++ b/bigcache_test.go @@ -34,21 +34,21 @@ func TestWriteAndGetOnCacheMulti(t *testing.T) { // given cache, _ := New(context.Background(), DefaultConfig(5*time.Second)) - keys := []string{"k1","k2","k3","k4","k5"} - values := [][]byte{[]byte("v1"),[]byte("v2"),[]byte("v3"),[]byte("v4"),[]byte("v5")} + keys := []string{"k1", "k2", "k3", "k4", "k5"} + values := [][]byte{[]byte("v1"), []byte("v2"), []byte("v3"), []byte("v4"), []byte("v5")} // when - for i,key := range keys{ - cache.Set(key,values[i]) - } + for i, key := range keys { + cache.Set(key, values[i]) + } cachedValues, err := cache.GetMulti(keys) // then noError(t, err) - for i,cachedValue := range cachedValues{ - assertEqual(t,values[i],cachedValue) - } + for i, cachedValue := range cachedValues { + assertEqual(t, values[i], cachedValue) + } } func TestAppendAndGetOnCache(t *testing.T) { From b8f5161daae5206a8700cc21a9349a8c9ad7d68b Mon Sep 17 00:00:00 2001 From: raeidish Date: Tue, 17 Oct 2023 10:42:43 +0200 Subject: [PATCH 07/16] wip get uses getWithoutLock --- shard.go | 20 +++++--------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/shard.go b/shard.go index 1bf47fa0..12d6cb07 100644 --- a/shard.go +++ b/shard.go @@ -61,22 +61,12 @@ func (s *cacheShard) getWithInfo(key string, hashedKey uint64) (entry []byte, re func (s *cacheShard) get(key string, hashedKey uint64) ([]byte, error) { s.lock.RLock() - wrappedEntry, err := s.getWrappedEntry(hashedKey) - if err != nil { - s.lock.RUnlock() - return nil, err - } - if entryKey := readKeyFromEntry(wrappedEntry); key != entryKey { - s.lock.RUnlock() - s.collision() - if s.isVerbose { - s.logger.Printf("Collision detected. Both %q and %q have the same hash %x", key, entryKey, hashedKey) - } - return nil, ErrEntryNotFound - } - entry := readEntry(wrappedEntry) + entry,err := s.getWithoutLock(key,hashedKey) s.lock.RUnlock() - s.hit(hashedKey) + + if err != nil{ + return nil,err + } return entry, nil } From 2a6bc3e8c669ca8b11b238381968a0784ef5c7bb Mon Sep 17 00:00:00 2001 From: raeidish <118611562+raeidish@users.noreply.github.com> Date: Tue, 17 Oct 2023 10:43:51 +0200 Subject: [PATCH 08/16] Update bigcache.go Co-authored-by: Tomasz Janiszewski --- bigcache.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bigcache.go b/bigcache.go index e8e0d8a9..837d4dfc 100644 --- a/bigcache.go +++ b/bigcache.go @@ -151,7 +151,7 @@ type keyInfo struct { // It returns an ErrEntryNotFound when // no entry exists for the given key. func (c *BigCache) GetMulti(keys []string) ([][]byte, error) { - shards := make(map[uint64][]keyInfo) + shards := make(map[uint64][]keyInfo, len(c.shards)) entries := make([][]byte, len(keys)) for i, key := range keys { From 556e314765fd6efb59a2b23c55352b3910622949 Mon Sep 17 00:00:00 2001 From: raeidish Date: Tue, 17 Oct 2023 15:40:25 +0200 Subject: [PATCH 09/16] wip return found entries+test --- bigcache.go | 17 ++++++---------- bigcache_test.go | 53 +++++++++++++++++++++++++++++++++++------------- 2 files changed, 45 insertions(+), 25 deletions(-) diff --git a/bigcache.go b/bigcache.go index 837d4dfc..b3d22100 100644 --- a/bigcache.go +++ b/bigcache.go @@ -137,7 +137,7 @@ func (c *BigCache) Get(key string) ([]byte, error) { return shard.get(key, hashedKey) } -// Used to sore information about keys in GetMulti function +// Used to store information about keys in GetMulti function // order is the index in the slice the data should go // hashedKey is the Sum64 hash of the key // key is the original key input @@ -148,9 +148,9 @@ type keyInfo struct { } // GetMulti reads entry for each of the keys. -// It returns an ErrEntryNotFound when -// no entry exists for the given key. -func (c *BigCache) GetMulti(keys []string) ([][]byte, error) { +// returns entries in the same order as the provided keys. +// if entry is not found for a given key, the index will contain nil +func (c *BigCache) GetMulti(keys []string) ([][]byte) { shards := make(map[uint64][]keyInfo, len(c.shards)) entries := make([][]byte, len(keys)) @@ -165,17 +165,12 @@ func (c *BigCache) GetMulti(keys []string) ([][]byte, error) { shard.lock.RLock() for i := range keyInfos { - entry, err := shard.getWithoutLock(keyInfos[i].key, keyInfos[i].hashedKey) - - if err != nil { - shard.lock.RUnlock() - return nil, err - } + entry,_ := shard.getWithoutLock(keyInfos[i].key, keyInfos[i].hashedKey) entries[keyInfos[i].order] = entry } shard.lock.RUnlock() } - return entries, nil + return entries } // GetWithInfo reads entry for the key with Response info. diff --git a/bigcache_test.go b/bigcache_test.go index 16487caf..47a7db62 100644 --- a/bigcache_test.go +++ b/bigcache_test.go @@ -31,24 +31,49 @@ func TestWriteAndGetOnCache(t *testing.T) { func TestWriteAndGetOnCacheMulti(t *testing.T) { t.Parallel() + for _, tc := range []struct { + keys []string + data [][]byte + want string + }{ + { + keys: []string{"k1","k2","k3","k4","k5"}, + data: [][]byte{ + blob('a',10), + blob('b',10), + blob('c',10), + blob('d',10), + blob('e',10), + }, + want: "Get all values ordered", + }, + { + keys: []string{"k1","k2","k3","k4","k5"}, + data: [][]byte{ + blob('a',10), + blob('b',10), + nil, + blob('d',10), + blob('e',10), + }, + want: "Get all values ordered with nil", + }, + }{ + t.Run(tc.want, func(t *testing.T) { + cache, _ := New(context.Background(), DefaultConfig(5*time.Second)) - // given - cache, _ := New(context.Background(), DefaultConfig(5*time.Second)) - keys := []string{"k1", "k2", "k3", "k4", "k5"} - values := [][]byte{[]byte("v1"), []byte("v2"), []byte("v3"), []byte("v4"), []byte("v5")} + for i := range tc.keys{ + if tc.data[i] != nil{ + cache.Set(tc.keys[i],tc.data[i]) + } + } - // when - for i, key := range keys { - cache.Set(key, values[i]) - } - cachedValues, err := cache.GetMulti(keys) + cachedValues := cache.GetMulti(tc.keys) - // then - noError(t, err) + assertEqual(t,tc.data,cachedValues) + }) - for i, cachedValue := range cachedValues { - assertEqual(t, values[i], cachedValue) - } + } } func TestAppendAndGetOnCache(t *testing.T) { From 142b251241ea09a02db16108d719fbf3cbafc39b Mon Sep 17 00:00:00 2001 From: raeidish Date: Tue, 17 Oct 2023 16:37:29 +0200 Subject: [PATCH 10/16] fixed race condition on with stats option --- shard.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shard.go b/shard.go index 91e6d2f3..02a2c598 100644 --- a/shard.go +++ b/shard.go @@ -85,7 +85,7 @@ func (s *cacheShard) getWithoutLock(key string, hashedKey uint64) ([]byte, error return nil, ErrEntryNotFound } entry := readEntry(wrappedEntry) - s.hitWithoutLock(hashedKey) + s.hit(hashedKey) return entry, nil } From a3691454d674aceac03f90d22484b64a500bae00 Mon Sep 17 00:00:00 2001 From: raeidish Date: Wed, 18 Oct 2023 19:51:47 +0200 Subject: [PATCH 11/16] added test for get multi with stats --- bigcache_test.go | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/bigcache_test.go b/bigcache_test.go index 47a7db62..23b305be 100644 --- a/bigcache_test.go +++ b/bigcache_test.go @@ -883,6 +883,46 @@ func TestWriteAndReadParallelSameKeyWithStats(t *testing.T) { assertEqual(t, ntest*n, int(cache.KeyMetadata(key).RequestCount)) } +func TestWriteAndReadManyParallelSameKeyWithStats(t *testing.T) { + t.Parallel() + + c := DefaultConfig(0) + c.StatsEnabled = true + + cache, _ := New(context.Background(), c) + var wg sync.WaitGroup + ntest := 1000 + n := 10 + wg.Add(n) + + keys := []string{"key1","key2","key3"} + values := [][]byte{blob('a', 1024),blob('b',1024),blob('c',1024)} + + for i := 0; i < ntest; i++ { + for j := range keys{ + assertEqual(t, nil, cache.Set(keys[j], values[j])) + } + } + + for j := 0; j < n; j++ { + go func() { + for i := 0; i < ntest; i++ { + v := cache.GetMulti(keys) + assertEqual(t, values, v) + } + wg.Done() + }() + } + + wg.Wait() + + assertEqual(t, Stats{Hits: int64(n * ntest* len(keys))}, cache.Stats()) + + for i := range keys{ + assertEqual(t, ntest*n, int(cache.KeyMetadata(keys[i]).RequestCount)) + } +} + func TestCacheReset(t *testing.T) { t.Parallel() From 3ab22f59eb4099b6ed441bb6e422db2ba59015fc Mon Sep 17 00:00:00 2001 From: raeidish Date: Wed, 18 Oct 2023 19:53:18 +0200 Subject: [PATCH 12/16] fix deadlock on with stats option --- bigcache.go | 11 ++++++++++- shard.go | 4 +++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/bigcache.go b/bigcache.go index b3d22100..8682638d 100644 --- a/bigcache.go +++ b/bigcache.go @@ -161,14 +161,23 @@ func (c *BigCache) GetMulti(keys []string) ([][]byte) { } for shardKey, keyInfos := range shards { + hits := make([]uint64,0,len(keyInfos)) shard := c.shards[shardKey] shard.lock.RLock() - for i := range keyInfos { entry,_ := shard.getWithoutLock(keyInfos[i].key, keyInfos[i].hashedKey) + + if entry != nil { + hits = append(hits, keyInfos[i].hashedKey) + } + entries[keyInfos[i].order] = entry } shard.lock.RUnlock() + + for i := range hits{ + shard.hit(hits[i]) + } } return entries } diff --git a/shard.go b/shard.go index 02a2c598..950b932d 100644 --- a/shard.go +++ b/shard.go @@ -68,6 +68,7 @@ func (s *cacheShard) get(key string, hashedKey uint64) ([]byte, error) { return nil,err } + s.hit(hashedKey) return entry, nil } @@ -84,8 +85,9 @@ func (s *cacheShard) getWithoutLock(key string, hashedKey uint64) ([]byte, error } return nil, ErrEntryNotFound } + entry := readEntry(wrappedEntry) - s.hit(hashedKey) + return entry, nil } From ee86c5d94bca7fd80afa275b856aad2fb35e7165 Mon Sep 17 00:00:00 2001 From: raeidish Date: Wed, 18 Oct 2023 19:53:52 +0200 Subject: [PATCH 13/16] go fmt --- bigcache.go | 22 +++++------ bigcache_test.go | 96 ++++++++++++++++++++++++------------------------ shard.go | 11 +++--- 3 files changed, 64 insertions(+), 65 deletions(-) diff --git a/bigcache.go b/bigcache.go index 8682638d..5e2e5907 100644 --- a/bigcache.go +++ b/bigcache.go @@ -150,7 +150,7 @@ type keyInfo struct { // GetMulti reads entry for each of the keys. // returns entries in the same order as the provided keys. // if entry is not found for a given key, the index will contain nil -func (c *BigCache) GetMulti(keys []string) ([][]byte) { +func (c *BigCache) GetMulti(keys []string) [][]byte { shards := make(map[uint64][]keyInfo, len(c.shards)) entries := make([][]byte, len(keys)) @@ -161,23 +161,23 @@ func (c *BigCache) GetMulti(keys []string) ([][]byte) { } for shardKey, keyInfos := range shards { - hits := make([]uint64,0,len(keyInfos)) + hits := make([]uint64, 0, len(keyInfos)) shard := c.shards[shardKey] shard.lock.RLock() for i := range keyInfos { - entry,_ := shard.getWithoutLock(keyInfos[i].key, keyInfos[i].hashedKey) + entry, _ := shard.getWithoutLock(keyInfos[i].key, keyInfos[i].hashedKey) - if entry != nil { - hits = append(hits, keyInfos[i].hashedKey) - } + if entry != nil { + hits = append(hits, keyInfos[i].hashedKey) + } - entries[keyInfos[i].order] = entry + entries[keyInfos[i].order] = entry } - shard.lock.RUnlock() + shard.lock.RUnlock() - for i := range hits{ - shard.hit(hits[i]) - } + for i := range hits { + shard.hit(hits[i]) + } } return entries } diff --git a/bigcache_test.go b/bigcache_test.go index 23b305be..8d3ac416 100644 --- a/bigcache_test.go +++ b/bigcache_test.go @@ -31,49 +31,49 @@ func TestWriteAndGetOnCache(t *testing.T) { func TestWriteAndGetOnCacheMulti(t *testing.T) { t.Parallel() - for _, tc := range []struct { - keys []string - data [][]byte - want string - }{ - { - keys: []string{"k1","k2","k3","k4","k5"}, - data: [][]byte{ - blob('a',10), - blob('b',10), - blob('c',10), - blob('d',10), - blob('e',10), - }, - want: "Get all values ordered", - }, - { - keys: []string{"k1","k2","k3","k4","k5"}, - data: [][]byte{ - blob('a',10), - blob('b',10), - nil, - blob('d',10), - blob('e',10), - }, - want: "Get all values ordered with nil", - }, - }{ + for _, tc := range []struct { + keys []string + data [][]byte + want string + }{ + { + keys: []string{"k1", "k2", "k3", "k4", "k5"}, + data: [][]byte{ + blob('a', 10), + blob('b', 10), + blob('c', 10), + blob('d', 10), + blob('e', 10), + }, + want: "Get all values ordered", + }, + { + keys: []string{"k1", "k2", "k3", "k4", "k5"}, + data: [][]byte{ + blob('a', 10), + blob('b', 10), + nil, + blob('d', 10), + blob('e', 10), + }, + want: "Get all values ordered with nil", + }, + } { t.Run(tc.want, func(t *testing.T) { - cache, _ := New(context.Background(), DefaultConfig(5*time.Second)) + cache, _ := New(context.Background(), DefaultConfig(5*time.Second)) - for i := range tc.keys{ - if tc.data[i] != nil{ - cache.Set(tc.keys[i],tc.data[i]) - } - } + for i := range tc.keys { + if tc.data[i] != nil { + cache.Set(tc.keys[i], tc.data[i]) + } + } - cachedValues := cache.GetMulti(tc.keys) + cachedValues := cache.GetMulti(tc.keys) - assertEqual(t,tc.data,cachedValues) - }) + assertEqual(t, tc.data, cachedValues) + }) - } + } } func TestAppendAndGetOnCache(t *testing.T) { @@ -894,14 +894,14 @@ func TestWriteAndReadManyParallelSameKeyWithStats(t *testing.T) { ntest := 1000 n := 10 wg.Add(n) - - keys := []string{"key1","key2","key3"} - values := [][]byte{blob('a', 1024),blob('b',1024),blob('c',1024)} + + keys := []string{"key1", "key2", "key3"} + values := [][]byte{blob('a', 1024), blob('b', 1024), blob('c', 1024)} for i := 0; i < ntest; i++ { - for j := range keys{ - assertEqual(t, nil, cache.Set(keys[j], values[j])) - } + for j := range keys { + assertEqual(t, nil, cache.Set(keys[j], values[j])) + } } for j := 0; j < n; j++ { @@ -916,11 +916,11 @@ func TestWriteAndReadManyParallelSameKeyWithStats(t *testing.T) { wg.Wait() - assertEqual(t, Stats{Hits: int64(n * ntest* len(keys))}, cache.Stats()) + assertEqual(t, Stats{Hits: int64(n * ntest * len(keys))}, cache.Stats()) - for i := range keys{ - assertEqual(t, ntest*n, int(cache.KeyMetadata(keys[i]).RequestCount)) - } + for i := range keys { + assertEqual(t, ntest*n, int(cache.KeyMetadata(keys[i]).RequestCount)) + } } func TestCacheReset(t *testing.T) { diff --git a/shard.go b/shard.go index 950b932d..4cc3adb3 100644 --- a/shard.go +++ b/shard.go @@ -61,14 +61,14 @@ func (s *cacheShard) getWithInfo(key string, hashedKey uint64) (entry []byte, re func (s *cacheShard) get(key string, hashedKey uint64) ([]byte, error) { s.lock.RLock() - entry,err := s.getWithoutLock(key,hashedKey) + entry, err := s.getWithoutLock(key, hashedKey) s.lock.RUnlock() - if err != nil{ - return nil,err - } + if err != nil { + return nil, err + } - s.hit(hashedKey) + s.hit(hashedKey) return entry, nil } @@ -87,7 +87,6 @@ func (s *cacheShard) getWithoutLock(key string, hashedKey uint64) ([]byte, error } entry := readEntry(wrappedEntry) - return entry, nil } From e89fb4292ed2f998e9980b802ce31a901005a88d Mon Sep 17 00:00:00 2001 From: raeidish Date: Wed, 18 Oct 2023 20:06:58 +0200 Subject: [PATCH 14/16] wip --- bigcache_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bigcache_test.go b/bigcache_test.go index 8d3ac416..e516f66e 100644 --- a/bigcache_test.go +++ b/bigcache_test.go @@ -896,7 +896,7 @@ func TestWriteAndReadManyParallelSameKeyWithStats(t *testing.T) { wg.Add(n) keys := []string{"key1", "key2", "key3"} - values := [][]byte{blob('a', 1024), blob('b', 1024), blob('c', 1024)} + values := [][]byte{blob('a', 64), blob('b', 64), blob('c', 64)} for i := 0; i < ntest; i++ { for j := range keys { From 793d8a031012cfae9e692a9adc9b97b3c26a2a3f Mon Sep 17 00:00:00 2001 From: raeidish Date: Wed, 18 Oct 2023 20:15:10 +0200 Subject: [PATCH 15/16] wip --- bigcache_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bigcache_test.go b/bigcache_test.go index e516f66e..4abfd7f8 100644 --- a/bigcache_test.go +++ b/bigcache_test.go @@ -891,7 +891,7 @@ func TestWriteAndReadManyParallelSameKeyWithStats(t *testing.T) { cache, _ := New(context.Background(), c) var wg sync.WaitGroup - ntest := 1000 + ntest := 100 n := 10 wg.Add(n) From c366663461d0caa2748de5bcf92b776ab43787cd Mon Sep 17 00:00:00 2001 From: raeidish Date: Mon, 23 Oct 2023 16:11:10 +0200 Subject: [PATCH 16/16] use one lock for hits --- bigcache.go | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/bigcache.go b/bigcache.go index 5e2e5907..ecfe81e2 100644 --- a/bigcache.go +++ b/bigcache.go @@ -175,9 +175,17 @@ func (c *BigCache) GetMulti(keys []string) [][]byte { } shard.lock.RUnlock() + if shard.statsEnabled{ + shard.lock.Lock() + } + for i := range hits { - shard.hit(hits[i]) + shard.hitWithoutLock(hits[i]) } + + if shard.statsEnabled{ + shard.lock.Unlock() + } } return entries }