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

feature add lru cache evict #30

Merged
merged 16 commits into from
Jan 11, 2024
1 change: 0 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ require (
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/hashicorp/golang-lru/v2 v2.0.6
github.com/pmezard/go-difflib v1.0.0 // indirect
golang.org/x/sync v0.1.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
Expand Down
2 changes: 0 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/r
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/ecodeclub/ekit v0.0.8-0.20230925161647-c5bfbd460261 h1:FunYsaj58DVk4iIBXeU8hwdbvlGS1hc7ZbWXOx/+Vj0=
github.com/ecodeclub/ekit v0.0.8-0.20230925161647-c5bfbd460261/go.mod h1:OqTojKeKFTxeeAAUwNIPKu339SRkX6KAuoK/8A5BCEs=
github.com/hashicorp/golang-lru/v2 v2.0.6 h1:3xi/Cafd1NaoEnS/yDssIiuVeDVywU0QdFGl3aQaQHM=
github.com/hashicorp/golang-lru/v2 v2.0.6/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
Expand Down
221 changes: 185 additions & 36 deletions memory/lru/cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,53 +27,202 @@

"github.com/ecodeclub/ecache"
"github.com/ecodeclub/ecache/internal/errs"
"github.com/hashicorp/golang-lru/v2/simplelru"
)

var (
_ ecache.Cache = (*Cache)(nil)
)

type entry struct {
key string
value any
expiresAt time.Time
}

func (e entry) isExpired() bool {
return !e.expiresAt.IsZero() && e.expiresAt.Before(time.Now())
}

type EvictCallback func(key string, value any)
Stone-afk marked this conversation as resolved.
Show resolved Hide resolved

type Option func(l *Cache)

func WithEvictCallback(callback func(k string, v any)) Option {
return func(l *Cache) {
l.callback = callback
}
}

func WithCycleInterval(interval time.Duration) Option {
return func(l *Cache) {
l.cycleInterval = interval
}
}

type Cache struct {
lock sync.RWMutex
client simplelru.LRUCache[string, any]
lock sync.RWMutex
capacity int
list *linkedList[entry]
data map[string]*element[entry]
callback EvictCallback
cycleInterval time.Duration
}

func NewCache(client simplelru.LRUCache[string, any]) *Cache {
return &Cache{
lock: sync.RWMutex{},
client: client,
func NewCache(capacity int, options ...Option) *Cache {
Stone-afk marked this conversation as resolved.
Show resolved Hide resolved
res := &Cache{
list: newLinkedList[entry](),
data: make(map[string]*element[entry], capacity),
capacity: capacity,
cycleInterval: time.Second * 10,
}
for _, opt := range options {
opt(res)
}
res.cleanCycle()
return res
}

func (c *Cache) cleanCycle() {
go func() {
ticker := time.NewTicker(c.cycleInterval)
for range ticker.C {
cnt := 0
c.lock.Lock()
limit := c.list.len() / 3
for elem, i := c.list.back(), 0; i < c.list.len(); i++ {
Stone-afk marked this conversation as resolved.
Show resolved Hide resolved
if elem.Value.isExpired() {
c.removeElement(elem)
}
elem = elem.prev
cnt++
if cnt >= limit {
break
}
}
c.lock.Unlock()
}
}()
}

func (c *Cache) pushEntry(key string, ent entry) bool {
if len(c.data) >= c.capacity && c.len() >= c.capacity {
if elem, ok := c.data[key]; ok {
elem.Value = ent
c.list.moveToFront(elem)
return false
}
c.removeOldest()

Check warning on line 114 in memory/lru/cache.go

View check run for this annotation

Codecov / codecov/patch

memory/lru/cache.go#L114

Added line #L114 was not covered by tests
}
if elem, ok := c.data[key]; ok {
elem.Value = ent
c.list.moveToFront(elem)
return false
}
elem := c.list.pushFront(ent)
c.data[key] = elem
return true
}

func (c *Cache) addTTL(key string, value any, expiration time.Duration) bool {
ent := entry{key: key, value: value,
expiresAt: time.Now().Add(expiration)}
return c.pushEntry(key, ent)
}

func (c *Cache) add(key string, value any) bool {
ent := entry{key: key, value: value}
return c.pushEntry(key, ent)
}

func (c *Cache) get(key string) (value any, ok bool) {
if elem, exist := c.data[key]; exist {
ent := elem.Value
if ent.isExpired() {
c.removeElement(elem)
return
}
c.list.moveToFront(elem)
return ent.value, true
}
return
}

func (c *Cache) removeOldest() {
if elem := c.list.back(); elem != nil {
c.removeElement(elem)
}

Check warning on line 153 in memory/lru/cache.go

View check run for this annotation

Codecov / codecov/patch

memory/lru/cache.go#L150-L153

Added lines #L150 - L153 were not covered by tests
}

func (c *Cache) removeElement(elem *element[entry]) {
c.list.removeElem(elem)
ent := elem.Value
c.delete(ent.key)
if c.callback != nil {
c.callback(ent.key, ent.value)
}
}

func (c *Cache) remove(key string) bool {
if elem, ok := c.data[key]; ok {
c.removeElement(elem)
return !elem.Value.isExpired()
}
return false

Check warning on line 170 in memory/lru/cache.go

View check run for this annotation

Codecov / codecov/patch

memory/lru/cache.go#L170

Added line #L170 was not covered by tests
}

func (c *Cache) contains(key string) (ok bool) {
elem, ok := c.data[key]
if ok {
if elem.Value.isExpired() {
c.removeElement(elem)
return false
}

Check warning on line 179 in memory/lru/cache.go

View check run for this annotation

Codecov / codecov/patch

memory/lru/cache.go#L177-L179

Added lines #L177 - L179 were not covered by tests
}
return ok
}

func (c *Cache) delete(key string) {
delete(c.data, key)
}

func (c *Cache) len() int {
var length int
for elem, i := c.list.back(), 0; i < c.list.len(); i++ {
if elem.Value.isExpired() {
c.removeElement(elem)
continue

Check warning on line 193 in memory/lru/cache.go

View check run for this annotation

Codecov / codecov/patch

memory/lru/cache.go#L192-L193

Added lines #L192 - L193 were not covered by tests
}
elem = elem.prev
length++
}
return length
}

// Set expiration 无效 由lru 统一控制过期时间
func (c *Cache) Set(ctx context.Context, key string, val any, expiration time.Duration) error {
c.lock.Lock()
defer c.lock.Unlock()

c.client.Add(key, val)
c.addTTL(key, val, expiration)
return nil
}

// SetNX expiration 无效 由lru 统一控制过期时间
func (c *Cache) SetNX(ctx context.Context, key string, val any, expiration time.Duration) (bool, error) {
c.lock.Lock()
defer c.lock.Unlock()

if c.client.Contains(key) {
if c.contains(key) {
return false, nil
}

c.client.Add(key, val)
c.addTTL(key, val, expiration)

return true, nil
}

func (c *Cache) Get(ctx context.Context, key string) (val ecache.Value) {
c.lock.RLock()
defer c.lock.RUnlock()
c.lock.Lock()
defer c.lock.Unlock()
var ok bool
val.Val, ok = c.client.Get(key)
val.Val, ok = c.get(key)
if !ok {
val.Err = errs.ErrKeyNotExist
}
Expand All @@ -86,12 +235,12 @@
defer c.lock.Unlock()

var ok bool
result.Val, ok = c.client.Get(key)
result.Val, ok = c.get(key)
if !ok {
result.Err = errs.ErrKeyNotExist
}

c.client.Add(key, val)
c.add(key, val)

return
}
Expand All @@ -105,11 +254,11 @@
if ctx.Err() != nil {
return n, ctx.Err()
}
_, ok := c.client.Get(k)
_, ok := c.get(k)
if !ok {
continue
}
if c.client.Remove(k) {
if c.remove(k) {
n++
} else {
return n, fmt.Errorf("%w: key = %s", errs.ErrDeleteKeyFailed, k)
Expand Down Expand Up @@ -137,12 +286,12 @@
ok bool
result = ecache.Value{}
)
result.Val, ok = c.client.Get(key)
result.Val, ok = c.get(key)
if !ok {
l := &list.ConcurrentList[ecache.Value]{
List: list.NewLinkedListOf[ecache.Value](c.anySliceToValueSlice(val...)),
}
c.client.Add(key, l)
c.add(key, l)
return int64(l.Len()), nil
}

Expand All @@ -156,7 +305,7 @@
return 0, err
}

c.client.Add(key, data)
c.add(key, data)
return int64(data.Len()), nil
}

Expand All @@ -167,7 +316,7 @@
var (
ok bool
)
val.Val, ok = c.client.Get(key)
val.Val, ok = c.get(key)
if !ok {
val.Err = errs.ErrKeyNotExist
return
Expand Down Expand Up @@ -197,7 +346,7 @@
ok bool
result = ecache.Value{}
)
result.Val, ok = c.client.Get(key)
result.Val, ok = c.get(key)
if !ok {
result.Val = set.NewMapSet[any](8)
}
Expand All @@ -210,7 +359,7 @@
for _, value := range members {
s.Add(value)
}
c.client.Add(key, s)
c.add(key, s)

return int64(len(s.Keys())), nil
}
Expand All @@ -219,7 +368,7 @@
c.lock.Lock()
defer c.lock.Unlock()

result, ok := c.client.Get(key)
result, ok := c.get(key)
if !ok {
return 0, errs.ErrKeyNotExist
}
Expand Down Expand Up @@ -247,9 +396,9 @@
ok bool
result = ecache.Value{}
)
result.Val, ok = c.client.Get(key)
result.Val, ok = c.get(key)
if !ok {
c.client.Add(key, value)
c.add(key, value)
return value, nil
}

Expand All @@ -259,7 +408,7 @@
}

newVal := incr + value
c.client.Add(key, newVal)
c.add(key, newVal)

return newVal, nil
}
Expand All @@ -272,9 +421,9 @@
ok bool
result = ecache.Value{}
)
result.Val, ok = c.client.Get(key)
result.Val, ok = c.get(key)
if !ok {
c.client.Add(key, -value)
c.add(key, -value)
return -value, nil
}

Expand All @@ -284,7 +433,7 @@
}

newVal := decr - value
c.client.Add(key, newVal)
c.add(key, newVal)

return newVal, nil
}
Expand All @@ -297,9 +446,9 @@
ok bool
result = ecache.Value{}
)
result.Val, ok = c.client.Get(key)
result.Val, ok = c.get(key)
if !ok {
c.client.Add(key, value)
c.add(key, value)
return value, nil
}

Expand All @@ -309,7 +458,7 @@
}

newVal := val + value
c.client.Add(key, newVal)
c.add(key, newVal)

return newVal, nil
}
Loading
Loading