Skip to content

Commit 78289f8

Browse files
authored
Merge pull request #38 from karlseguin/DeletePrefix
Delete prefix
2 parents 569ae60 + 79f9dcd commit 78289f8

File tree

4 files changed

+77
-3
lines changed

4 files changed

+77
-3
lines changed

bucket.go

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package ccache
22

33
import (
4+
"strings"
45
"sync"
56
"time"
67
)
@@ -26,22 +27,61 @@ func (b *bucket) set(key string, value interface{}, duration time.Duration) (*It
2627
expires := time.Now().Add(duration).UnixNano()
2728
item := newItem(key, value, expires)
2829
b.Lock()
29-
defer b.Unlock()
3030
existing := b.lookup[key]
3131
b.lookup[key] = item
32+
b.Unlock()
3233
return item, existing
3334
}
3435

3536
func (b *bucket) delete(key string) *Item {
3637
b.Lock()
37-
defer b.Unlock()
3838
item := b.lookup[key]
3939
delete(b.lookup, key)
40+
b.Unlock()
4041
return item
4142
}
4243

44+
// This is an expensive operation, so we do what we can to optimize it and limit
45+
// the impact it has on concurrent operations. Specifically, we:
46+
// 1 - Do an initial iteration to collect matches. This allows us to do the
47+
// "expensive" prefix check (on all values) using only a read-lock
48+
// 2 - Do a second iteration, under write lock, for the matched results to do
49+
// the actual deletion
50+
51+
// Also, this is the only place where the Bucket is aware of cache detail: the
52+
// deletables channel. Passing it here lets us avoid iterating over matched items
53+
// again in the cache. Further, we pass item to deletables BEFORE actually removing
54+
// the item from the map. I'm pretty sure this is 100% fine, but it is unique.
55+
// (We do this so that the write to the channel is under the read lock and not the
56+
// write lock)
57+
func (b *bucket) deletePrefix(prefix string, deletables chan *Item) int {
58+
lookup := b.lookup
59+
items := make([]*Item, 0, len(lookup)/10)
60+
61+
b.RLock()
62+
for key, item := range lookup {
63+
if strings.HasPrefix(key, prefix) {
64+
deletables <- item
65+
items = append(items, item)
66+
}
67+
}
68+
b.RUnlock()
69+
70+
if len(items) == 0 {
71+
// avoid the write lock if we can
72+
return 0
73+
}
74+
75+
b.Lock()
76+
for _, item := range items {
77+
delete(lookup, item.key)
78+
}
79+
b.Unlock()
80+
return len(items)
81+
}
82+
4383
func (b *bucket) clear() {
4484
b.Lock()
45-
defer b.Unlock()
4685
b.lookup = make(map[string]*Item)
86+
b.Unlock()
4787
}

cache.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,14 @@ func (c *Cache) ItemCount() int {
4545
return count
4646
}
4747

48+
func (c *Cache) DeletePrefix(prefix string) int {
49+
count := 0
50+
for _, b := range c.buckets {
51+
count += b.deletePrefix(prefix, c.deletables)
52+
}
53+
return count
54+
}
55+
4856
// Get an item from the cache. Returns nil if the item wasn't found.
4957
// This can return an expired item. Use item.Expired() to see if the item
5058
// is expired and item.TTL() to see how long until the item expires (which

cache_test.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,29 @@ func (_ CacheTests) DeletesAValue() {
2828
Expect(cache.ItemCount()).To.Equal(1)
2929
}
3030

31+
func (_ CacheTests) DeletesAPrefix() {
32+
cache := New(Configure())
33+
Expect(cache.ItemCount()).To.Equal(0)
34+
35+
cache.Set("aaa", "1", time.Minute)
36+
cache.Set("aab", "2", time.Minute)
37+
cache.Set("aac", "3", time.Minute)
38+
cache.Set("ac", "4", time.Minute)
39+
cache.Set("z5", "7", time.Minute)
40+
Expect(cache.ItemCount()).To.Equal(5)
41+
42+
Expect(cache.DeletePrefix("9a")).To.Equal(0)
43+
Expect(cache.ItemCount()).To.Equal(5)
44+
45+
Expect(cache.DeletePrefix("aa")).To.Equal(3)
46+
Expect(cache.Get("aaa")).To.Equal(nil)
47+
Expect(cache.Get("aab")).To.Equal(nil)
48+
Expect(cache.Get("aac")).To.Equal(nil)
49+
Expect(cache.Get("ac").Value()).To.Equal("4")
50+
Expect(cache.Get("z5").Value()).To.Equal("7")
51+
Expect(cache.ItemCount()).To.Equal(2)
52+
}
53+
3154
func (_ CacheTests) OnDeleteCallbackCalled() {
3255
onDeleteFnCalled := false
3356
onDeleteFn := func(item *Item) {

readme.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,9 @@ item, err := cache.Fetch("user:4", time.Minute * 10, func() (interface{}, error)
9191
cache.Delete("user:4")
9292
```
9393

94+
### DeletePrefix
95+
`DeletePrefix` deletes all keys matching the provided prefix. Returns the number of keys removed.
96+
9497
### Clear
9598
`Clear` clears the cache. This method is **not** thread safe. It is meant to be used from tests.
9699

0 commit comments

Comments
 (0)