Merge pull request #38 from karlseguin/DeletePrefix

Delete prefix
This commit is contained in:
Karl Seguin 2020-01-23 13:03:42 +08:00 committed by GitHub
commit 78289f8f0b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 77 additions and 3 deletions

View file

@ -1,6 +1,7 @@
package ccache
import (
"strings"
"sync"
"time"
)
@ -26,22 +27,61 @@ func (b *bucket) set(key string, value interface{}, duration time.Duration) (*It
expires := time.Now().Add(duration).UnixNano()
item := newItem(key, value, expires)
b.Lock()
defer b.Unlock()
existing := b.lookup[key]
b.lookup[key] = item
b.Unlock()
return item, existing
}
func (b *bucket) delete(key string) *Item {
b.Lock()
defer b.Unlock()
item := b.lookup[key]
delete(b.lookup, key)
b.Unlock()
return item
}
// This is an expensive operation, so we do what we can to optimize it and limit
// the impact it has on concurrent operations. Specifically, we:
// 1 - Do an initial iteration to collect matches. This allows us to do the
// "expensive" prefix check (on all values) using only a read-lock
// 2 - Do a second iteration, under write lock, for the matched results to do
// the actual deletion
// Also, this is the only place where the Bucket is aware of cache detail: the
// deletables channel. Passing it here lets us avoid iterating over matched items
// again in the cache. Further, we pass item to deletables BEFORE actually removing
// the item from the map. I'm pretty sure this is 100% fine, but it is unique.
// (We do this so that the write to the channel is under the read lock and not the
// write lock)
func (b *bucket) deletePrefix(prefix string, deletables chan *Item) int {
lookup := b.lookup
items := make([]*Item, 0, len(lookup)/10)
b.RLock()
for key, item := range lookup {
if strings.HasPrefix(key, prefix) {
deletables <- item
items = append(items, item)
}
}
b.RUnlock()
if len(items) == 0 {
// avoid the write lock if we can
return 0
}
b.Lock()
for _, item := range items {
delete(lookup, item.key)
}
b.Unlock()
return len(items)
}
func (b *bucket) clear() {
b.Lock()
defer b.Unlock()
b.lookup = make(map[string]*Item)
b.Unlock()
}

View file

@ -45,6 +45,14 @@ func (c *Cache) ItemCount() int {
return count
}
func (c *Cache) DeletePrefix(prefix string) int {
count := 0
for _, b := range c.buckets {
count += b.deletePrefix(prefix, c.deletables)
}
return count
}
// Get an item from the cache. Returns nil if the item wasn't found.
// This can return an expired item. Use item.Expired() to see if the item
// is expired and item.TTL() to see how long until the item expires (which

View file

@ -28,6 +28,29 @@ func (_ CacheTests) DeletesAValue() {
Expect(cache.ItemCount()).To.Equal(1)
}
func (_ CacheTests) DeletesAPrefix() {
cache := New(Configure())
Expect(cache.ItemCount()).To.Equal(0)
cache.Set("aaa", "1", time.Minute)
cache.Set("aab", "2", time.Minute)
cache.Set("aac", "3", time.Minute)
cache.Set("ac", "4", time.Minute)
cache.Set("z5", "7", time.Minute)
Expect(cache.ItemCount()).To.Equal(5)
Expect(cache.DeletePrefix("9a")).To.Equal(0)
Expect(cache.ItemCount()).To.Equal(5)
Expect(cache.DeletePrefix("aa")).To.Equal(3)
Expect(cache.Get("aaa")).To.Equal(nil)
Expect(cache.Get("aab")).To.Equal(nil)
Expect(cache.Get("aac")).To.Equal(nil)
Expect(cache.Get("ac").Value()).To.Equal("4")
Expect(cache.Get("z5").Value()).To.Equal("7")
Expect(cache.ItemCount()).To.Equal(2)
}
func (_ CacheTests) OnDeleteCallbackCalled() {
onDeleteFnCalled := false
onDeleteFn := func(item *Item) {

View file

@ -91,6 +91,9 @@ item, err := cache.Fetch("user:4", time.Minute * 10, func() (interface{}, error)
cache.Delete("user:4")
```
### DeletePrefix
`DeletePrefix` deletes all keys matching the provided prefix. Returns the number of keys removed.
### Clear
`Clear` clears the cache. This method is **not** thread safe. It is meant to be used from tests.