Skip to content

Commit 75a65a8

Browse files
authored
Implemented new Append() method (#203)
1 parent fcb069f commit 75a65a8

File tree

3 files changed

+193
-0
lines changed

3 files changed

+193
-0
lines changed

bigcache.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,15 @@ func (c *BigCache) Set(key string, entry []byte) error {
134134
return shard.set(key, hashedKey, entry)
135135
}
136136

137+
// Append appends entry under the key if key exists, otherwise
138+
// it will set the key (same behaviour as Set()). With Append() you can
139+
// concatenate multiple entries under the same key in an lock optimized way.
140+
func (c *BigCache) Append(key string, entry []byte) error {
141+
hashedKey := c.hash.Sum64(key)
142+
shard := c.getShard(hashedKey)
143+
return shard.append(key, hashedKey, entry)
144+
}
145+
137146
// Delete removes the key
138147
func (c *BigCache) Delete(key string) error {
139148
hashedKey := c.hash.Sum64(key)

bigcache_test.go

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"math"
77
"math/rand"
88
"runtime"
9+
"strings"
910
"sync"
1011
"testing"
1112
"time"
@@ -27,6 +28,119 @@ func TestWriteAndGetOnCache(t *testing.T) {
2728
assertEqual(t, value, cachedValue)
2829
}
2930

31+
func TestAppendAndGetOnCache(t *testing.T) {
32+
t.Parallel()
33+
34+
// given
35+
cache, _ := NewBigCache(DefaultConfig(5 * time.Second))
36+
key := "key"
37+
value1 := make([]byte, 50)
38+
rand.Read(value1)
39+
value2 := make([]byte, 50)
40+
rand.Read(value2)
41+
value3 := make([]byte, 50)
42+
rand.Read(value3)
43+
44+
// when
45+
_, err := cache.Get(key)
46+
47+
// then
48+
assertEqual(t, ErrEntryNotFound, err)
49+
50+
// when
51+
cache.Append(key, value1)
52+
cachedValue, err := cache.Get(key)
53+
54+
// then
55+
noError(t, err)
56+
assertEqual(t, value1, cachedValue)
57+
58+
// when
59+
cache.Append(key, value2)
60+
cachedValue, err = cache.Get(key)
61+
62+
// then
63+
noError(t, err)
64+
expectedValue := value1
65+
expectedValue = append(expectedValue, value2...)
66+
assertEqual(t, expectedValue, cachedValue)
67+
68+
// when
69+
cache.Append(key, value3)
70+
cachedValue, err = cache.Get(key)
71+
72+
// then
73+
noError(t, err)
74+
expectedValue = value1
75+
expectedValue = append(expectedValue, value2...)
76+
expectedValue = append(expectedValue, value3...)
77+
assertEqual(t, expectedValue, cachedValue)
78+
}
79+
80+
// TestAppendRandomly does simultaneous appends to check for corruption errors.
81+
func TestAppendRandomly(t *testing.T) {
82+
t.Parallel()
83+
84+
c := Config{
85+
Shards: 1,
86+
LifeWindow: 5 * time.Second,
87+
CleanWindow: 1 * time.Second,
88+
MaxEntriesInWindow: 1000 * 10 * 60,
89+
MaxEntrySize: 500,
90+
StatsEnabled: true,
91+
Verbose: true,
92+
Hasher: newDefaultHasher(),
93+
HardMaxCacheSize: 1,
94+
Logger: DefaultLogger(),
95+
}
96+
cache, err := NewBigCache(c)
97+
noError(t, err)
98+
99+
nKeys := 5
100+
nAppendsPerKey := 2000
101+
nWorker := 10
102+
var keys []string
103+
for i := 0; i < nKeys; i++ {
104+
for j := 0; j < nAppendsPerKey; j++ {
105+
keys = append(keys, fmt.Sprintf("key%d", i))
106+
}
107+
}
108+
rand.Shuffle(len(keys), func(i, j int) {
109+
keys[i], keys[j] = keys[j], keys[i]
110+
})
111+
112+
jobs := make(chan string, len(keys))
113+
for _, key := range keys {
114+
jobs <- key
115+
}
116+
close(jobs)
117+
118+
var wg sync.WaitGroup
119+
for i := 0; i < nWorker; i++ {
120+
wg.Add(1)
121+
go func() {
122+
for {
123+
key, ok := <-jobs
124+
if !ok {
125+
break
126+
}
127+
cache.Append(key, []byte(key))
128+
}
129+
wg.Done()
130+
}()
131+
}
132+
wg.Wait()
133+
134+
assertEqual(t, nKeys, cache.Len())
135+
for i := 0; i < nKeys; i++ {
136+
key := fmt.Sprintf("key%d", i)
137+
expectedValue := []byte(strings.Repeat(key, nAppendsPerKey))
138+
cachedValue, err := cache.Get(key)
139+
noError(t, err)
140+
assertEqual(t, expectedValue, cachedValue)
141+
}
142+
}
143+
30144
func TestConstructCacheWithDefaultHasher(t *testing.T) {
31145
t.Parallel()
32146

shard.go

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,24 @@ func (s *cacheShard) get(key string, hashedKey uint64) ([]byte, error) {
8484
return entry, nil
8585
}
8686

87+
func (s *cacheShard) getWithoutLock(key string, hashedKey uint64) ([]byte, error) {
88+
wrappedEntry, err := s.getWrappedEntry(hashedKey)
89+
if err != nil {
90+
return nil, err
91+
}
92+
if entryKey := readKeyFromEntry(wrappedEntry); key != entryKey {
93+
s.collision()
94+
if s.isVerbose {
95+
s.logger.Printf("Collision detected. Both %q and %q have the same hash %x", key, entryKey, hashedKey)
96+
}
97+
return nil, ErrEntryNotFound
98+
}
99+
entry := readEntry(wrappedEntry)
100+
s.hitWithoutLock(hashedKey)
101+
102+
return entry, nil
103+
}
104+
87105
func (s *cacheShard) getWrappedEntry(hashedKey uint64) ([]byte, error) {
88106
itemIndex := s.hashmap[hashedKey]
89107

@@ -131,6 +149,51 @@ func (s *cacheShard) set(key string, hashedKey uint64, entry []byte) error {
131149
}
132150
}
133151

152+
func (s *cacheShard) setWithoutLock(key string, hashedKey uint64, entry []byte) error {
153+
currentTimestamp := uint64(s.clock.epoch())
154+
155+
if previousIndex := s.hashmap[hashedKey]; previousIndex != 0 {
156+
if previousEntry, err := s.entries.Get(int(previousIndex)); err == nil {
157+
resetKeyFromEntry(previousEntry)
158+
}
159+
}
160+
161+
if oldestEntry, err := s.entries.Peek(); err == nil {
162+
s.onEvict(oldestEntry, currentTimestamp, s.removeOldestEntry)
163+
}
164+
165+
w := wrapEntry(currentTimestamp, hashedKey, key, entry, &s.entryBuffer)
166+
167+
for {
168+
if index, err := s.entries.Push(w); err == nil {
169+
s.hashmap[hashedKey] = uint32(index)
170+
return nil
171+
}
172+
if s.removeOldestEntry(NoSpace) != nil {
173+
return fmt.Errorf("entry is bigger than max shard size")
174+
}
175+
}
176+
}
177+
178+
func (s *cacheShard) append(key string, hashedKey uint64, entry []byte) error {
179+
s.lock.Lock()
180+
var newEntry []byte
181+
oldEntry, err := s.getWithoutLock(key, hashedKey)
182+
if err != nil {
183+
if err != ErrEntryNotFound {
184+
s.lock.Unlock()
185+
return err
186+
}
187+
} else {
188+
newEntry = oldEntry
189+
}
190+
191+
newEntry = append(newEntry, entry...)
192+
err = s.setWithoutLock(key, hashedKey, newEntry)
193+
s.lock.Unlock()
194+
return err
195+
}
196+
134197
func (s *cacheShard) del(hashedKey uint64) error {
135198
// Optimistic pre-check using only readlock
136199
s.lock.RLock()
@@ -287,6 +350,13 @@ func (s *cacheShard) hit(key uint64) {
287350
}
288351
}
289352

353+
func (s *cacheShard) hitWithoutLock(key uint64) {
354+
atomic.AddInt64(&s.stats.Hits, 1)
355+
if s.statsEnabled {
356+
s.hashmapStats[key]++
357+
}
358+
}
359+
290360
func (s *cacheShard) miss() {
291361
atomic.AddInt64(&s.stats.Misses, 1)
292362
}

0 commit comments

Comments
 (0)