Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Delete Cache Memories with Wildcard (*) #184

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ require (
github.com/go-playground/validator/v10 v10.11.1
github.com/go-redis/redis/v8 v8.11.5
github.com/golang-jwt/jwt v3.2.2+incompatible
github.com/golang/mock v1.6.0
github.com/google/uuid v1.3.0
github.com/labstack/echo-contrib v0.11.0
github.com/labstack/echo/v4 v4.10.2
Expand All @@ -20,7 +19,6 @@ require (
github.com/stretchr/testify v1.8.2
github.com/valyala/fasttemplate v1.2.2
go.mongodb.org/mongo-driver v1.8.2
golang.org/x/mod v0.8.0
golang.org/x/sync v0.1.0
golang.org/x/tools v0.6.0
gorm.io/datatypes v1.0.5
Expand Down Expand Up @@ -71,6 +69,7 @@ require (
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect
golang.org/x/crypto v0.17.0 // indirect
golang.org/x/exp v0.0.0-20221031165847-c99f073a8326 // indirect
golang.org/x/mod v0.8.0 // indirect
golang.org/x/net v0.17.0 // indirect
golang.org/x/sys v0.15.0 // indirect
golang.org/x/text v0.14.0 // indirect
Expand Down
1 change: 0 additions & 1 deletion go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,6 @@ github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8=
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
Expand Down
65 changes: 52 additions & 13 deletions store/memory/memory.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,27 +23,28 @@
package memory

import (
"strings"
"sync"
"time"

"github.com/alphadose/haxmap"
)

type Cache interface {
GetMemory(k string) (interface{}, bool)
GetMemory(key string) (interface{}, bool)
SetMemory(key string, value any, duration time.Duration)
DelMemory(key string)
CloseMemory()
}

// memoryCache stores arbitrary data with ttl.
type memoryCache struct {
keys *haxmap.Map[string, Key]
keys *haxmap.Map[string, data]
done chan struct{}
}

// A Key represents arbitrary data with ttl.
type Key struct {
// data represents an arbitrary value with ttl.
type data struct {
value any
ttl int64 // unix nano
}
Expand All @@ -59,7 +60,7 @@ func NewMemoryCache() Cache {
// XXX: IMPORTANT - Run the ttl cleaning process in every 60 seconds.
ttlCleaningInterval := 60 * time.Second

h := haxmap.New[string, Key]()
h := haxmap.New[string, data]()
if h == nil {
panic("failed to initialize the memory!")
}
Expand All @@ -78,8 +79,8 @@ func NewMemoryCache() Cache {
case <-ticker.C:
now := time.Now().UnixNano()
// O(N) iteration. It is linear time complexity.
memoryDBConn.keys.ForEach(func(k string, item Key) bool {
if item.ttl > 0 && now > item.ttl {
memoryDBConn.keys.ForEach(func(k string, d data) bool {
if d.ttl > 0 && now > d.ttl {
memoryDBConn.keys.Del(k)
}

Expand All @@ -97,17 +98,17 @@ func NewMemoryCache() Cache {
}

// GetMemory Get gets the value for the given key.
func (mem *memoryCache) GetMemory(k string) (interface{}, bool) {
key, exists := mem.keys.Get(k)
func (mem *memoryCache) GetMemory(key string) (interface{}, bool) {
d, exists := mem.keys.Get(key)
if !exists {
return nil, false
}

if key.ttl > 0 && time.Now().UnixNano() > key.ttl {
if d.ttl > 0 && time.Now().UnixNano() > d.ttl {
return nil, false
}

return key.value, true
return d.value, true
}

// SetMemory Set sets a value for the given key with an expiration duration.
Expand All @@ -119,15 +120,53 @@ func (mem *memoryCache) SetMemory(key string, value any, duration time.Duration)
expires = time.Now().Add(duration).UnixNano()
}

mem.keys.Set(key, Key{
mem.keys.Set(key, data{
value: value,
ttl: expires,
})
}

// DelMemory Del deletes the key and its value from the memory cache.
// If the key has a wildcard (`*`), it will delete all keys that match the wildcard.
func (mem *memoryCache) DelMemory(key string) {
mem.keys.Del(key)

if !strings.Contains(key, "*") {
// Delete by a normal key.
mem.keys.Del(key)
return
}

// Delete by wildcard key.
var keys []string
mem.keys.ForEach(func(k string, _ data) bool {
if matchWildCard([]rune(k), []rune(key)) {
keys = append(keys, k)
}
return true
})
if len(keys) > 0 {
mem.keys.Del(keys...)
}
}

func matchWildCard(str, pattern []rune) bool {

if len(pattern) == 0 {
return len(str) == 0 // Return true finally if both are empty after the rescursive matching.
}

if pattern[0] == '*' {
// Match with no wildcard pattern, if it doesn't match, move to the next character.
return matchWildCard(str, pattern[1:]) ||
(len(str) > 0 && matchWildCard(str[1:], pattern))
}

if len(str) == 0 || str[0] != pattern[0] {
return false
}

// Recurse with the rest of the string and the pattern.
return matchWildCard(str[1:], pattern[1:])
}

// CloseMemory Close closes the memory cache and frees up resources.
Expand Down
101 changes: 101 additions & 0 deletions store/memory/memory_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
package memory

import (
"fmt"
"sync/atomic"
"testing"
"time"
Expand Down Expand Up @@ -86,6 +87,79 @@ func TestDelete(t *testing.T) {
}
}

func TestDeleteWithWildCard(t *testing.T) {
type keyVal struct {
key string
value interface{}
ttl time.Duration
}
type kvs struct {
keyVal
found bool
}
features := []keyVal{
{key: "hello_world_1", value: "world_0", ttl: time.Hour},
{key: "hello_world_2", value: "world_1", ttl: time.Hour},
{key: "hello_world_3", value: "world_2", ttl: time.Hour},
{key: "hello_1_world", value: "world_3", ttl: time.Hour},
{key: "hello_2_world", value: "world_4", ttl: time.Hour},
{key: "hello_3_world", value: "world_5", ttl: time.Hour},
{key: "1_hello_world", value: "world_6", ttl: time.Hour},
{key: "2_hello_world", value: "world_7", ttl: time.Hour},
{key: "3_hello_world", value: "world_8", ttl: time.Hour},
}

tests := []struct {
name string
kvs []kvs
wildcard string
}{
{name: "delete multi keys by tail wildcard", kvs: []kvs{
{features[0], false}, {features[1], false}, {features[2], false},
{features[3], true}, {features[4], true}, {features[5], true},
{features[6], true}, {features[7], true}, {features[8], true},
}, wildcard: "hello_world_*"},
{name: "delete multi keys by middle wildcard", kvs: []kvs{
{features[0], true}, {features[1], true}, {features[2], true},
{features[3], false}, {features[4], false}, {features[5], false},
{features[6], true}, {features[7], true}, {features[8], true},
}, wildcard: "hello_*_world"},
{name: "delete multi keys by head wildcard", kvs: []kvs{
{features[0], true}, {features[1], true}, {features[2], true},
{features[3], true}, {features[4], true}, {features[5], true},
{features[6], false}, {features[7], false}, {features[8], false},
}, wildcard: "*_hello_world"},
{name: "delete multi keys by exact wildcard", kvs: []kvs{
{features[0], false}, {features[1], false}, {features[2], false},
{features[3], false}, {features[4], false}, {features[5], false},
{features[6], false}, {features[7], false}, {features[8], false},
}, wildcard: "*"},
{name: "delete multi keys by two intermittent wildcards", kvs: []kvs{
{features[0], false}, {features[1], false}, {features[2], false},
{features[3], false}, {features[4], false}, {features[5], false},
{features[6], false}, {features[7], false}, {features[8], false},
}, wildcard: "*_*"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
m := NewMemoryCache()

for _, kv := range tt.kvs {
m.SetMemory(kv.key, kv.value, kv.ttl)
}

m.DelMemory(tt.wildcard)

for _, kv := range tt.kvs {
_, found := m.GetMemory(kv.key)
if found != kv.found {
t.Errorf("key %s, expected found %v, got %v", kv.key, kv.found, found)
}
}
})
}
}

func BenchmarkNew(b *testing.B) {
b.ReportAllocs()

Expand Down Expand Up @@ -165,3 +239,30 @@ func BenchmarkDel(b *testing.B) {
}
})
}

// WARN: It takes about 30s to complete this benchmark with 1s `benchtime`.
func BenchmarkDeleteWithWildCard(b *testing.B) {

setupKeys := func(m Cache, size int) {
for i := 0; i < size; i++ {
key := fmt.Sprintf("hello_%d", i)
m.SetMemory(key, fmt.Sprintf("world_%d", i), time.Hour)
}
}

sizes := []int{100, 1000, 10000}
for _, size := range sizes {
b.Run(fmt.Sprintf("delete %d keys", size), func(b *testing.B) {
m := NewMemoryCache()
b.ResetTimer()

for i := 0; i < b.N; i++ {
b.StopTimer()
setupKeys(m, size)
b.StartTimer()

m.DelMemory("hello_*")
}
})
}
}
Loading