Skip to content

Commit a25552a

Browse files
committed
Attempt to make Clear concurrency-safe
This is an attempt at fixing #81 without imposing a performance hit on the cache's "normal" (get/set/fetch) activity. Calling "Clear" is now considerably more expensive.
1 parent 3505243 commit a25552a

File tree

5 files changed

+107
-13
lines changed

5 files changed

+107
-13
lines changed

bucket.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,8 +98,10 @@ func (b *bucket[T]) deletePrefix(prefix string, deletables chan *Item[T]) int {
9898
}, deletables)
9999
}
100100

101+
// we expect the caller to have acquired a write lock
101102
func (b *bucket[T]) clear() {
102-
b.Lock()
103+
for _, item := range b.lookup {
104+
item.promotions = -2
105+
}
103106
b.lookup = make(map[string]*Item[T])
104-
b.Unlock()
105107
}

cache.go

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,24 @@ func (c *Cache[T]) bucket(key string) *bucket[T] {
206206
return c.buckets[h.Sum32()&c.bucketMask]
207207
}
208208

209+
func (c *Cache[T]) halted(fn func()) {
210+
c.halt()
211+
defer c.unhalt()
212+
fn()
213+
}
214+
215+
func (c *Cache[T]) halt() {
216+
for _, bucket := range c.buckets {
217+
bucket.Lock()
218+
}
219+
}
220+
221+
func (c *Cache[T]) unhalt() {
222+
for _, bucket := range c.buckets {
223+
bucket.Unlock()
224+
}
225+
}
226+
209227
func (c *Cache[T]) worker() {
210228
dropped := 0
211229
cc := c.control
@@ -236,11 +254,22 @@ func (c *Cache[T]) worker() {
236254
}
237255
msg.done <- struct{}{}
238256
case controlClear:
239-
for _, bucket := range c.buckets {
240-
bucket.clear()
241-
}
242-
c.size = 0
243-
c.list = NewList[*Item[T]]()
257+
c.halted(func() {
258+
promotables := c.promotables
259+
for len(promotables) > 0 {
260+
<-promotables
261+
}
262+
deletables := c.deletables
263+
for len(deletables) > 0 {
264+
<-deletables
265+
}
266+
267+
for _, bucket := range c.buckets {
268+
bucket.clear()
269+
}
270+
c.size = 0
271+
c.list = NewList[*Item[T]]()
272+
})
244273
msg.done <- struct{}{}
245274
case controlGetSize:
246275
msg.res <- c.size

cache_test.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"math/rand"
55
"sort"
66
"strconv"
7+
"sync"
78
"sync/atomic"
89
"testing"
910
"time"
@@ -361,6 +362,40 @@ func Test_ConcurrentStop(t *testing.T) {
361362
}
362363
}
363364

365+
func Test_ConcurrentClearAndSet(t *testing.T) {
366+
for i := 0; i < 100; i++ {
367+
var stop atomic.Bool
368+
var wg sync.WaitGroup
369+
370+
cache := New(Configure[string]())
371+
r := func() {
372+
for !stop.Load() {
373+
cache.Set("a", "a", time.Minute)
374+
}
375+
wg.Done()
376+
}
377+
go r()
378+
wg.Add(1)
379+
cache.Clear()
380+
stop.Store(true)
381+
wg.Wait()
382+
time.Sleep(time.Millisecond)
383+
cache.SyncUpdates()
384+
385+
known := make(map[string]struct{})
386+
for node := cache.list.Head; node != nil; node = node.Next {
387+
known[node.Value.key] = struct{}{}
388+
}
389+
390+
for _, bucket := range cache.buckets {
391+
for key := range bucket.lookup {
392+
_, exists := known[key]
393+
assert.True(t, exists)
394+
}
395+
}
396+
}
397+
}
398+
364399
type SizedItem struct {
365400
id int
366401
s int64

layeredbucket.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,9 +111,8 @@ func (b *layeredBucket[T]) forEachFunc(primary string, matches func(key string,
111111
}
112112
}
113113

114+
// we expect the caller to have acquired a write lock
114115
func (b *layeredBucket[T]) clear() {
115-
b.Lock()
116-
defer b.Unlock()
117116
for _, bucket := range b.buckets {
118117
bucket.clear()
119118
}

layeredcache.go

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,24 @@ func (c *LayeredCache[T]) bucket(key string) *layeredBucket[T] {
196196
return c.buckets[h.Sum32()&c.bucketMask]
197197
}
198198

199+
func (c *LayeredCache[T]) halted(fn func()) {
200+
c.halt()
201+
defer c.unhalt()
202+
fn()
203+
}
204+
205+
func (c *LayeredCache[T]) halt() {
206+
for _, bucket := range c.buckets {
207+
bucket.Lock()
208+
}
209+
}
210+
211+
func (c *LayeredCache[T]) unhalt() {
212+
for _, bucket := range c.buckets {
213+
bucket.Unlock()
214+
}
215+
}
216+
199217
func (c *LayeredCache[T]) promote(item *Item[T]) {
200218
c.promotables <- item
201219
}
@@ -230,11 +248,22 @@ func (c *LayeredCache[T]) worker() {
230248
}
231249
msg.done <- struct{}{}
232250
case controlClear:
233-
for _, bucket := range c.buckets {
234-
bucket.clear()
251+
promotables := c.promotables
252+
for len(promotables) > 0 {
253+
<-promotables
235254
}
236-
c.size = 0
237-
c.list = NewList[*Item[T]]()
255+
deletables := c.deletables
256+
for len(deletables) > 0 {
257+
<-deletables
258+
}
259+
260+
c.halted(func() {
261+
for _, bucket := range c.buckets {
262+
bucket.clear()
263+
}
264+
c.size = 0
265+
c.list = NewList[*Item[T]]()
266+
})
238267
msg.done <- struct{}{}
239268
case controlGetSize:
240269
msg.res <- c.size

0 commit comments

Comments
 (0)