From 093af89af906985375c1dafb4d4ebee5304f1fa8 Mon Sep 17 00:00:00 2001 From: Stone-afk <1711865140@qq.com> Date: Sat, 23 Dec 2023 12:30:42 +0800 Subject: [PATCH 01/16] feature add lru cache evict --- cacheevict/cache.go | 317 ++++++++++++ cacheevict/cache_test.go | 983 ++++++++++++++++++++++++++++++++++++ cacheevict/lru/list.go | 187 +++++++ cacheevict/lru/list_test.go | 370 ++++++++++++++ cacheevict/lru/lru.go | 317 ++++++++++++ cacheevict/lru/lru_test.go | 341 +++++++++++++ cacheevict/types.go | 39 ++ 7 files changed, 2554 insertions(+) create mode 100644 cacheevict/cache.go create mode 100644 cacheevict/cache_test.go create mode 100644 cacheevict/lru/list.go create mode 100644 cacheevict/lru/list_test.go create mode 100644 cacheevict/lru/lru.go create mode 100644 cacheevict/lru/lru_test.go create mode 100644 cacheevict/types.go diff --git a/cacheevict/cache.go b/cacheevict/cache.go new file mode 100644 index 0000000..ed240cc --- /dev/null +++ b/cacheevict/cache.go @@ -0,0 +1,317 @@ +// Copyright 2023 ecodeclub +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cacheevict + +import ( + "context" + "errors" + "fmt" + "sync" + "time" + + "github.com/ecodeclub/ecache" + "github.com/ecodeclub/ecache/internal/errs" + "github.com/ecodeclub/ekit/list" + "github.com/ecodeclub/ekit/set" +) + +var ( + _ ecache.Cache = (*EvictCache)(nil) +) + +type Option func(l *EvictCache) + +func WithEvictStrategy(strategy EvictStrategy[string, any]) Option { + return func(l *EvictCache) { + l.strategy = strategy + } +} + +type EvictCache struct { + lock sync.RWMutex + strategy EvictStrategy[string, any] +} + +func NewEvictCache(strategy EvictStrategy[string, any]) *EvictCache { + return &EvictCache{ + strategy: strategy, + } +} + +func (c *EvictCache) Set(ctx context.Context, key string, val any, expiration time.Duration) error { + c.lock.Lock() + defer c.lock.Unlock() + c.strategy.AddTTL(key, val, expiration) + return nil +} + +// SetNX 由 strategy 统一控制过期时间 +func (c *EvictCache) SetNX(ctx context.Context, key string, val any, expiration time.Duration) (bool, error) { + c.lock.Lock() + defer c.lock.Unlock() + + if c.strategy.Contains(key) { + return false, nil + } + + c.strategy.AddTTL(key, val, expiration) + + return true, nil +} + +func (c *EvictCache) Get(ctx context.Context, key string) (val ecache.Value) { + c.lock.Lock() + defer c.lock.Unlock() + var ok bool + val.Val, ok = c.strategy.Get(key) + if !ok { + val.Err = errs.ErrKeyNotExist + } + + return +} + +func (c *EvictCache) GetSet(ctx context.Context, key string, val string) (result ecache.Value) { + c.lock.Lock() + defer c.lock.Unlock() + + var ok bool + result.Val, ok = c.strategy.Get(key) + if !ok { + result.Err = errs.ErrKeyNotExist + } + + c.strategy.Add(key, val) + + return +} + +func (c *EvictCache) Delete(ctx context.Context, key ...string) (int64, error) { + c.lock.Lock() + defer c.lock.Unlock() + + n := int64(0) + for _, k := range key { + if ctx.Err() != nil { + return n, ctx.Err() + } + _, ok := c.strategy.Get(k) + if !ok { + continue + } + if c.strategy.Remove(k) { + n++ + } else { + return n, fmt.Errorf("%w: key = %s", errs.ErrDeleteKeyFailed, k) + } + } + return n, nil +} + +// anySliceToValueSlice 公共转换 +func (c *EvictCache) anySliceToValueSlice(data ...any) []ecache.Value { + newVal := make([]ecache.Value, len(data), cap(data)) + for key, value := range data { + anyVal := ecache.Value{} + anyVal.Val = value + newVal[key] = anyVal + } + return newVal +} + +func (c *EvictCache) LPush(ctx context.Context, key string, val ...any) (int64, error) { + c.lock.Lock() + defer c.lock.Unlock() + + var ( + ok bool + result = ecache.Value{} + ) + result.Val, ok = c.strategy.Get(key) + if !ok { + l := &list.ConcurrentList[ecache.Value]{ + List: list.NewLinkedListOf[ecache.Value](c.anySliceToValueSlice(val...)), + } + c.strategy.Add(key, l) + return int64(l.Len()), nil + } + + data, ok := result.Val.(list.List[ecache.Value]) + if !ok { + return 0, errors.New("当前key不是list类型") + } + + err := data.Append(c.anySliceToValueSlice(val)...) + if err != nil { + return 0, err + } + + c.strategy.Add(key, data) + return int64(data.Len()), nil +} + +func (c *EvictCache) LPop(ctx context.Context, key string) (val ecache.Value) { + c.lock.Lock() + defer c.lock.Unlock() + + var ( + ok bool + ) + val.Val, ok = c.strategy.Get(key) + if !ok { + val.Err = errs.ErrKeyNotExist + return + } + + data, ok := val.Val.(list.List[ecache.Value]) + if !ok { + val.Err = errors.New("当前key不是list类型") + return + } + + value, err := data.Delete(0) + if err != nil { + val.Err = err + return + } + + val = value + return +} + +func (c *EvictCache) SAdd(ctx context.Context, key string, members ...any) (int64, error) { + c.lock.Lock() + defer c.lock.Unlock() + + var ( + ok bool + result = ecache.Value{} + ) + result.Val, ok = c.strategy.Get(key) + if !ok { + result.Val = set.NewMapSet[any](8) + } + + s, ok := result.Val.(set.Set[any]) + if !ok { + return 0, errors.New("当前key已存在不是set类型") + } + + for _, value := range members { + s.Add(value) + } + c.strategy.Add(key, s) + + return int64(len(s.Keys())), nil +} + +func (c *EvictCache) SRem(ctx context.Context, key string, members ...any) (int64, error) { + c.lock.Lock() + defer c.lock.Unlock() + + result, ok := c.strategy.Get(key) + if !ok { + return 0, errs.ErrKeyNotExist + } + + s, ok := result.(set.Set[any]) + if !ok { + return 0, errors.New("当前key已存在不是set类型") + } + + var rems int64 + for _, member := range members { + if s.Exist(member) { + s.Delete(member) + rems++ + } + } + return rems, nil +} + +func (c *EvictCache) IncrBy(ctx context.Context, key string, value int64) (int64, error) { + c.lock.Lock() + defer c.lock.Unlock() + + var ( + ok bool + result = ecache.Value{} + ) + result.Val, ok = c.strategy.Get(key) + if !ok { + c.strategy.Add(key, value) + return value, nil + } + + incr, err := result.Int64() + if err != nil { + return 0, errors.New("当前key不是int64类型") + } + + newVal := incr + value + c.strategy.Add(key, newVal) + + return newVal, nil +} + +func (c *EvictCache) DecrBy(ctx context.Context, key string, value int64) (int64, error) { + c.lock.Lock() + defer c.lock.Unlock() + + var ( + ok bool + result = ecache.Value{} + ) + result.Val, ok = c.strategy.Get(key) + if !ok { + c.strategy.Add(key, -value) + return -value, nil + } + + decr, err := result.Int64() + if err != nil { + return 0, errors.New("当前key不是int64类型") + } + + newVal := decr - value + c.strategy.Add(key, newVal) + + return newVal, nil +} + +func (c *EvictCache) IncrByFloat(ctx context.Context, key string, value float64) (float64, error) { + c.lock.Lock() + defer c.lock.Unlock() + + var ( + ok bool + result = ecache.Value{} + ) + result.Val, ok = c.strategy.Get(key) + if !ok { + c.strategy.Add(key, value) + return value, nil + } + + val, err := result.Float64() + if err != nil { + return 0, errors.New("当前key不是float64类型") + } + + newVal := val + value + c.strategy.Add(key, newVal) + + return newVal, nil +} diff --git a/cacheevict/cache_test.go b/cacheevict/cache_test.go new file mode 100644 index 0000000..44e2e4f --- /dev/null +++ b/cacheevict/cache_test.go @@ -0,0 +1,983 @@ +// Copyright 2023 ecodeclub +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cacheevict + +import ( + "context" + "errors" + "testing" + "time" + + "github.com/ecodeclub/ecache" + "github.com/ecodeclub/ecache/cacheevict/lru" + "github.com/ecodeclub/ecache/internal/errs" + "github.com/ecodeclub/ekit/list" + "github.com/ecodeclub/ekit/set" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestEvictCache_Set(t *testing.T) { + evictCounter := 0 + onEvicted := func(key string, value any) { + evictCounter++ + } + strategy := lru.NewLRU[string, any]( + lru.WithCapacity[string, any](5), + lru.WithCallback[string, any](onEvicted)) + + testCase := []struct { + name string + after func(t *testing.T) + + key string + val string + expiration time.Duration + + wantErr error + }{ + { + name: "set value", + after: func(t *testing.T) { + result, ok := strategy.Get("test") + assert.Equal(t, true, ok) + assert.Equal(t, "hello ecache", result.(string)) + assert.Equal(t, true, strategy.Remove("test")) + }, + key: "test", + val: "hello ecache", + expiration: time.Minute, + }, + } + + for _, tc := range testCase { + t.Run(tc.name, func(t *testing.T) { + ctx, cancelFunc := context.WithTimeout(context.Background(), time.Second*5) + defer cancelFunc() + c := NewEvictCache(strategy) + + err := c.Set(ctx, tc.key, tc.val, tc.expiration) + require.NoError(t, err) + tc.after(t) + }) + } +} + +func TestEvictCache_Get(t *testing.T) { + evictCounter := 0 + onEvicted := func(key string, value any) { + evictCounter++ + } + strategy := lru.NewLRU[string, any]( + lru.WithCapacity[string, any](5), + lru.WithCallback[string, any](onEvicted)) + + testCase := []struct { + name string + before func(t *testing.T) + after func(t *testing.T) + + key string + + wantVal string + wantErr error + }{ + { + name: "get value", + before: func(t *testing.T) { + assert.Equal(t, false, strategy.Add("test", "hello ecache")) + }, + after: func(t *testing.T) { + assert.Equal(t, true, strategy.Remove("test")) + }, + key: "test", + wantVal: "hello ecache", + }, + { + name: "get expired value", + before: func(t *testing.T) { + assert.Equal(t, false, + strategy.AddTTL("test", "hello ecache", time.Second)) + }, + after: func(t *testing.T) { + time.Sleep(time.Second) + _, ok := strategy.Get("test") + assert.Equal(t, false, ok) + }, + key: "test", + wantVal: "hello ecache", + }, + { + name: "get value err", + before: func(t *testing.T) {}, + after: func(t *testing.T) {}, + key: "test", + wantErr: errs.ErrKeyNotExist, + }, + } + + for _, tc := range testCase { + t.Run(tc.name, func(t *testing.T) { + ctx, cancelFunc := context.WithTimeout(context.Background(), time.Second*5) + defer cancelFunc() + c := NewEvictCache(strategy) + + tc.before(t) + result := c.Get(ctx, tc.key) + val, err := result.String() + assert.Equal(t, tc.wantVal, val) + assert.Equal(t, tc.wantErr, err) + tc.after(t) + }) + } +} + +func TestEvictCache_SetNX(t *testing.T) { + evictCounter := 0 + onEvicted := func(key string, value any) { + evictCounter++ + } + strategy := lru.NewLRU[string, any]( + lru.WithCapacity[string, any](1), + lru.WithCallback[string, any](onEvicted)) + + testCase := []struct { + name string + before func(t *testing.T) + after func(t *testing.T) + + key string + val string + expire time.Duration + wantVal bool + }{ + { + name: "setnx value", + before: func(t *testing.T) {}, + after: func(t *testing.T) {}, + key: "test", + val: "hello ecache", + expire: time.Minute, + wantVal: true, + }, + { + name: "setnx value exist", + before: func(t *testing.T) { + assert.Equal(t, false, strategy.Add("test", "hello ecache")) + }, + after: func(t *testing.T) { + assert.Equal(t, true, strategy.Remove("test")) + }, + key: "test", + val: "hello world", + expire: time.Minute, + wantVal: false, + }, + { + name: "setnx expired value", + before: func(t *testing.T) { + assert.Equal(t, false, strategy.AddTTL("test", "hello ecache", time.Second)) + }, + after: func(t *testing.T) { + time.Sleep(time.Second) + assert.Equal(t, false, strategy.Remove("test")) + }, + key: "test", + val: "hello world", + expire: time.Minute, + wantVal: false, + }, + } + + for _, tc := range testCase { + t.Run(tc.name, func(t *testing.T) { + ctx, cancelFunc := context.WithTimeout(context.Background(), time.Second*5) + defer cancelFunc() + c := NewEvictCache(strategy) + + tc.before(t) + result, err := c.SetNX(ctx, tc.key, tc.val, tc.expire) + assert.Equal(t, tc.wantVal, result) + require.NoError(t, err) + tc.after(t) + }) + } +} + +func TestEvictCache_GetSet(t *testing.T) { + evictCounter := 0 + onEvicted := func(key string, value any) { + evictCounter++ + } + strategy := lru.NewLRU[string, any]( + lru.WithCapacity[string, any](5), + lru.WithCallback[string, any](onEvicted)) + + testCase := []struct { + name string + before func(t *testing.T) + after func(t *testing.T) + + key string + val string + wantVal string + wantErr error + }{ + { + name: "getset value", + before: func(t *testing.T) { + assert.Equal(t, false, strategy.Add("test", "hello ecache")) + }, + after: func(t *testing.T) { + result, ok := strategy.Get("test") + assert.Equal(t, true, ok) + assert.Equal(t, "hello world", result) + assert.Equal(t, true, strategy.Remove("test")) + }, + key: "test", + val: "hello world", + wantVal: "hello ecache", + }, + { + name: "getset value not key error", + before: func(t *testing.T) {}, + after: func(t *testing.T) { + result, ok := strategy.Get("test") + assert.Equal(t, true, ok) + assert.Equal(t, "hello world", result) + assert.Equal(t, true, strategy.Remove("test")) + }, + key: "test", + val: "hello world", + wantErr: errs.ErrKeyNotExist, + }, + } + + for _, tc := range testCase { + t.Run(tc.name, func(t *testing.T) { + ctx, cancelFunc := context.WithTimeout(context.Background(), time.Second*5) + defer cancelFunc() + c := NewEvictCache(strategy) + + tc.before(t) + result := c.GetSet(ctx, tc.key, tc.val) + val, err := result.String() + assert.Equal(t, tc.wantVal, val) + assert.Equal(t, tc.wantErr, err) + tc.after(t) + }) + } +} + +func TestEvictCache_Delete(t *testing.T) { + cache, err := newCache() + require.NoError(t, err) + + testCases := []struct { + name string + before func(ctx context.Context, t *testing.T, cache ecache.Cache) + + ctxFunc func() context.Context + key []string + + wantN int64 + wantErr error + }{ + { + name: "delete expired key", + before: func(ctx context.Context, t *testing.T, cache ecache.Cache) { + require.NoError(t, cache.Set(ctx, "name", "Alex", 0)) + }, + ctxFunc: func() context.Context { + return context.Background() + }, + key: []string{"name"}, + wantN: 0, + }, + { + name: "delete single existed key", + before: func(ctx context.Context, t *testing.T, cache ecache.Cache) { + require.NoError(t, cache.Set(ctx, "name", "Alex", 10*time.Second)) + }, + ctxFunc: func() context.Context { + return context.Background() + }, + key: []string{"name"}, + wantN: 1, + }, + { + name: "delete single does not existed key", + before: func(ctx context.Context, t *testing.T, cache ecache.Cache) {}, + ctxFunc: func() context.Context { + return context.Background() + }, + key: []string{"notExistedKey"}, + }, + { + name: "delete multiple expired keys", + before: func(ctx context.Context, t *testing.T, cache ecache.Cache) { + require.NoError(t, cache.Set(ctx, "name", "Alex", 0)) + require.NoError(t, cache.Set(ctx, "age", 18, 0)) + }, + ctxFunc: func() context.Context { + return context.Background() + }, + key: []string{"name", "age"}, + wantN: 0, + }, + { + name: "delete multiple existed keys", + before: func(ctx context.Context, t *testing.T, cache ecache.Cache) { + require.NoError(t, cache.Set(ctx, "name", "Alex", 10*time.Second)) + require.NoError(t, cache.Set(ctx, "age", 18, 10*time.Second)) + }, + ctxFunc: func() context.Context { + return context.Background() + }, + key: []string{"name", "age"}, + wantN: 2, + }, + { + name: "delete multiple do not existed keys", + before: func(ctx context.Context, t *testing.T, cache ecache.Cache) {}, + ctxFunc: func() context.Context { + return context.Background() + }, + key: []string{"name", "age"}, + }, + { + name: "delete multiple keys, some do not expired keys", + before: func(ctx context.Context, t *testing.T, cache ecache.Cache) { + require.NoError(t, cache.Set(ctx, "name", "Alex", 0)) + require.NoError(t, cache.Set(ctx, "age", 18, 0)) + require.NoError(t, cache.Set(ctx, "gender", "male", 0)) + }, + ctxFunc: func() context.Context { + return context.Background() + }, + key: []string{"name", "age", "gender", "addr"}, + wantN: 0, + }, + { + name: "delete multiple keys, some do not existed keys", + before: func(ctx context.Context, t *testing.T, cache ecache.Cache) { + require.NoError(t, cache.Set(ctx, "name", "Alex", 10*time.Second)) + require.NoError(t, cache.Set(ctx, "age", 18, 10*time.Second)) + require.NoError(t, cache.Set(ctx, "gender", "male", 10*time.Second)) + }, + ctxFunc: func() context.Context { + return context.Background() + }, + key: []string{"name", "age", "gender", "addr"}, + wantN: 3, + }, + { + name: "timeout", + before: func(ctx context.Context, t *testing.T, cache ecache.Cache) {}, + ctxFunc: func() context.Context { + timeout := time.Millisecond * 100 + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + time.Sleep(timeout * 2) + return ctx + }, + key: []string{"name", "age", "addr"}, + wantErr: context.DeadlineExceeded, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + ctx := tc.ctxFunc() + tc.before(ctx, t, cache) + n, err := cache.Delete(ctx, tc.key...) + if err != nil { + assert.ErrorIs(t, err, tc.wantErr) + return + } + assert.Equal(t, tc.wantN, n) + }) + } +} + +func TestEvictCache_LPush(t *testing.T) { + evictCounter := 0 + onEvicted := func(key string, value any) { + evictCounter++ + } + strategy := lru.NewLRU[string, any]( + lru.WithCapacity[string, any](5), + lru.WithCallback[string, any](onEvicted)) + + testCase := []struct { + name string + before func(t *testing.T) + after func(t *testing.T) + + key string + val []any + wantVal int64 + wantErr error + }{ + { + name: "lpush value", + before: func(t *testing.T) {}, + after: func(t *testing.T) { + assert.Equal(t, true, strategy.Remove("test")) + }, + key: "test", + val: []any{"hello ecache"}, + wantVal: 1, + }, + { + name: "lpush multiple value", + before: func(t *testing.T) {}, + after: func(t *testing.T) { + assert.Equal(t, true, strategy.Remove("test")) + }, + key: "test", + val: []any{"hello ecache", "hello world"}, + wantVal: 2, + }, + { + name: "lpush value exists", + before: func(t *testing.T) { + val := ecache.Value{} + val.Val = "hello ecache" + l := &list.ConcurrentList[ecache.Value]{ + List: list.NewLinkedListOf[ecache.Value]([]ecache.Value{val}), + } + assert.Equal(t, false, strategy.Add("test", l)) + }, + after: func(t *testing.T) { + assert.Equal(t, true, strategy.Remove("test")) + }, + key: "test", + val: []any{"hello world"}, + wantVal: 2, + }, + { + name: "lpush value not type", + before: func(t *testing.T) { + assert.Equal(t, false, strategy.Add("test", "string")) + }, + after: func(t *testing.T) { + assert.Equal(t, true, strategy.Remove("test")) + }, + key: "test", + val: []any{"hello ecache"}, + wantErr: errors.New("当前key不是list类型"), + }, + } + + for _, tc := range testCase { + t.Run(tc.name, func(t *testing.T) { + ctx, cancelFunc := context.WithTimeout(context.Background(), time.Second*5) + defer cancelFunc() + c := NewEvictCache(strategy) + + tc.before(t) + length, err := c.LPush(ctx, tc.key, tc.val...) + assert.Equal(t, tc.wantVal, length) + assert.Equal(t, tc.wantErr, err) + tc.after(t) + }) + } +} + +func TestEvictCache_LPop(t *testing.T) { + evictCounter := 0 + onEvicted := func(key string, value any) { + evictCounter++ + } + strategy := lru.NewLRU[string, any]( + lru.WithCapacity[string, any](5), + lru.WithCallback[string, any](onEvicted)) + + testCase := []struct { + name string + before func(t *testing.T) + after func(t *testing.T) + + key string + wantVal string + wantErr error + }{ + { + name: "lpop value", + before: func(t *testing.T) { + val := ecache.Value{} + val.Val = "hello ecache" + l := &list.ConcurrentList[ecache.Value]{ + List: list.NewLinkedListOf[ecache.Value]([]ecache.Value{val}), + } + assert.Equal(t, false, strategy.Add("test", l)) + }, + after: func(t *testing.T) { + assert.Equal(t, true, strategy.Remove("test")) + }, + key: "test", + wantVal: "hello ecache", + }, + { + name: "lpop value not nil", + before: func(t *testing.T) { + val := ecache.Value{} + val.Val = "hello ecache" + val2 := ecache.Value{} + val2.Val = "hello world" + l := &list.ConcurrentList[ecache.Value]{ + List: list.NewLinkedListOf[ecache.Value]([]ecache.Value{val, val2}), + } + assert.Equal(t, false, strategy.Add("test", l)) + }, + after: func(t *testing.T) { + val, ok := strategy.Get("test") + assert.Equal(t, true, ok) + result, ok := val.(list.List[ecache.Value]) + assert.Equal(t, true, ok) + assert.Equal(t, 1, result.Len()) + value, err := result.Delete(0) + assert.NoError(t, err) + assert.Equal(t, "hello world", value.Val) + assert.NoError(t, value.Err) + + assert.Equal(t, true, strategy.Remove("test")) + }, + key: "test", + wantVal: "hello ecache", + }, + { + name: "lpop value type error", + before: func(t *testing.T) { + assert.Equal(t, false, strategy.Add("test", "hello world")) + }, + after: func(t *testing.T) { + assert.Equal(t, true, strategy.Remove("test")) + }, + key: "test", + wantErr: errors.New("当前key不是list类型"), + }, + { + name: "lpop not key", + before: func(t *testing.T) {}, + after: func(t *testing.T) {}, + key: "test", + wantErr: errs.ErrKeyNotExist, + }, + } + + for _, tc := range testCase { + t.Run(tc.name, func(t *testing.T) { + ctx, cancelFunc := context.WithTimeout(context.Background(), time.Second*5) + defer cancelFunc() + c := NewEvictCache(strategy) + + tc.before(t) + val := c.LPop(ctx, tc.key) + result, err := val.String() + assert.Equal(t, tc.wantVal, result) + assert.Equal(t, tc.wantErr, err) + tc.after(t) + }) + } +} + +func TestEvictCache_SAdd(t *testing.T) { + evictCounter := 0 + onEvicted := func(key string, value any) { + evictCounter++ + } + strategy := lru.NewLRU[string, any]( + lru.WithCapacity[string, any](5), + lru.WithCallback[string, any](onEvicted)) + + testCase := []struct { + name string + before func(t *testing.T) + after func(t *testing.T) + + key string + val []any + wantVal int64 + wantErr error + }{ + { + name: "sadd value", + before: func(t *testing.T) {}, + after: func(t *testing.T) { + assert.Equal(t, true, strategy.Remove("test")) + }, + key: "test", + val: []any{"hello ecache", "hello world"}, + wantVal: 2, + }, + { + name: "sadd value exist", + before: func(t *testing.T) { + s := set.NewMapSet[any](8) + s.Add("hello world") + + assert.Equal(t, false, strategy.Add("test", s)) + }, + after: func(t *testing.T) { + assert.Equal(t, true, strategy.Remove("test")) + }, + key: "test", + val: []any{"hello ecache"}, + wantVal: 2, + }, + { + name: "sadd value type err", + before: func(t *testing.T) { + assert.Equal(t, false, strategy.Add("test", "string")) + }, + after: func(t *testing.T) { + assert.Equal(t, true, strategy.Remove("test")) + }, + key: "test", + val: []any{"hello"}, + wantErr: errors.New("当前key已存在不是set类型"), + }, + } + + for _, tc := range testCase { + t.Run(tc.name, func(t *testing.T) { + ctx, cancelFunc := context.WithTimeout(context.Background(), time.Second*5) + defer cancelFunc() + c := NewEvictCache(strategy) + + tc.before(t) + val, err := c.SAdd(ctx, tc.key, tc.val...) + assert.Equal(t, tc.wantVal, val) + assert.Equal(t, tc.wantErr, err) + tc.after(t) + }) + } +} + +func TestCache_SRem(t *testing.T) { + evictCounter := 0 + onEvicted := func(key string, value any) { + evictCounter++ + } + strategy := lru.NewLRU[string, any]( + lru.WithCapacity[string, any](5), + lru.WithCallback[string, any](onEvicted)) + + testCase := []struct { + name string + before func(t *testing.T) + after func(t *testing.T) + + key string + val []any + + wantVal int64 + wantErr error + }{ + { + name: "srem value", + before: func(t *testing.T) { + s := set.NewMapSet[any](8) + + s.Add("hello world") + s.Add("hello ecache") + + assert.Equal(t, false, strategy.Add("test", s)) + }, + after: func(t *testing.T) { + assert.Equal(t, true, strategy.Remove("test")) + }, + key: "test", + val: []any{"hello world"}, + wantVal: 1, + }, + { + name: "srem value ignore", + before: func(t *testing.T) { + s := set.NewMapSet[any](8) + s.Add("hello world") + + assert.Equal(t, false, strategy.Add("test", s)) + }, + after: func(t *testing.T) { + assert.Equal(t, true, strategy.Remove("test")) + }, + key: "test", + val: []any{"hello ecache"}, + wantVal: 0, + }, + { + name: "srem value nil", + before: func(t *testing.T) {}, + after: func(t *testing.T) {}, + key: "test", + val: []any{"hello world"}, + wantErr: errs.ErrKeyNotExist, + }, + { + name: "srem value type error", + before: func(t *testing.T) { + assert.Equal(t, false, strategy.Add("test", int64(1))) + }, + after: func(t *testing.T) { + assert.Equal(t, true, strategy.Remove("test")) + }, + key: "test", + val: []any{"hello world"}, + wantErr: errors.New("当前key已存在不是set类型"), + }, + } + + for _, tc := range testCase { + t.Run(tc.name, func(t *testing.T) { + defer tc.after(t) + ctx, cancelFunc := context.WithTimeout(context.Background(), time.Second*5) + defer cancelFunc() + c := NewEvictCache(strategy) + + tc.before(t) + val, err := c.SRem(ctx, tc.key, tc.val...) + assert.Equal(t, tc.wantErr, err) + assert.Equal(t, tc.wantVal, val) + }) + } +} + +func TestEvictCache_IncrBy(t *testing.T) { + evictCounter := 0 + onEvicted := func(key string, value any) { + evictCounter++ + } + strategy := lru.NewLRU[string, any]( + lru.WithCapacity[string, any](5), + lru.WithCallback[string, any](onEvicted)) + + testCase := []struct { + name string + before func(t *testing.T) + after func(t *testing.T) + + key string + val int64 + wantVal int64 + wantErr error + }{ + { + name: "incrby value", + before: func(t *testing.T) {}, + after: func(t *testing.T) { + assert.Equal(t, true, strategy.Remove("test")) + }, + key: "test", + val: 1, + wantVal: 1, + }, + { + name: "incrby value add", + before: func(t *testing.T) { + assert.Equal(t, false, strategy.Add("test", int64(1))) + }, + after: func(t *testing.T) { + assert.Equal(t, true, strategy.Remove("test")) + }, + key: "test", + val: 1, + wantVal: 2, + }, + { + name: "incrby value type error", + before: func(t *testing.T) { + assert.Equal(t, false, strategy.Add("test", 12.62)) + }, + after: func(t *testing.T) { + assert.Equal(t, true, strategy.Remove("test")) + }, + key: "test", + val: 1, + wantErr: errors.New("当前key不是int64类型"), + }, + } + + for _, tc := range testCase { + t.Run(tc.name, func(t *testing.T) { + ctx, cancelFunc := context.WithTimeout(context.Background(), time.Second*5) + defer cancelFunc() + c := NewEvictCache(strategy) + + tc.before(t) + result, err := c.IncrBy(ctx, tc.key, tc.val) + assert.Equal(t, tc.wantVal, result) + assert.Equal(t, tc.wantErr, err) + tc.after(t) + }) + } +} + +func TestEvictCache_DecrBy(t *testing.T) { + evictCounter := 0 + onEvicted := func(key string, value any) { + evictCounter++ + } + strategy := lru.NewLRU[string, any]( + lru.WithCapacity[string, any](5), + lru.WithCallback[string, any](onEvicted)) + + testCase := []struct { + name string + before func(t *testing.T) + after func(t *testing.T) + + key string + val int64 + wantVal int64 + wantErr error + }{ + { + name: "decrby value", + before: func(t *testing.T) {}, + after: func(t *testing.T) { + assert.Equal(t, true, strategy.Remove("test")) + }, + key: "test", + val: 1, + wantVal: -1, + }, + { + name: "decrby old value", + before: func(t *testing.T) { + assert.Equal(t, false, strategy.Add("test", int64(3))) + }, + after: func(t *testing.T) { + assert.Equal(t, true, strategy.Remove("test")) + }, + key: "test", + val: 2, + wantVal: 1, + }, + { + name: "decrby value type error", + before: func(t *testing.T) { + assert.Equal(t, false, strategy.Add("test", 3.156)) + }, + after: func(t *testing.T) { + assert.Equal(t, true, strategy.Remove("test")) + }, + key: "test", + val: 1, + wantErr: errors.New("当前key不是int64类型"), + }, + } + + for _, tc := range testCase { + t.Run(tc.name, func(t *testing.T) { + ctx, cancelFunc := context.WithTimeout(context.Background(), time.Second*5) + defer cancelFunc() + c := NewEvictCache(strategy) + + tc.before(t) + val, err := c.DecrBy(ctx, tc.key, tc.val) + assert.Equal(t, tc.wantVal, val) + assert.Equal(t, tc.wantErr, err) + tc.after(t) + }) + } +} + +func TestEvictCache_IncrByFloat(t *testing.T) { + evictCounter := 0 + onEvicted := func(key string, value any) { + evictCounter++ + } + strategy := lru.NewLRU[string, any]( + lru.WithCapacity[string, any](5), + lru.WithCallback[string, any](onEvicted)) + + testCase := []struct { + name string + before func(t *testing.T) + after func(t *testing.T) + + key string + val float64 + wantVal float64 + wantErr error + }{ + { + name: "incrbyfloat value", + before: func(t *testing.T) {}, + after: func(t *testing.T) { + assert.Equal(t, true, strategy.Remove("test")) + }, + key: "test", + val: 2.0, + wantVal: 2.0, + }, + { + name: "incrbyfloat decr value", + before: func(t *testing.T) { + assert.Equal(t, false, strategy.Add("test", 3.1)) + }, + after: func(t *testing.T) { + assert.Equal(t, true, strategy.Remove("test")) + }, + key: "test", + val: -2.0, + wantVal: 1.1, + }, + { + name: "incrbyfloat value type error", + before: func(t *testing.T) { + assert.Equal(t, false, strategy.Add("test", "hello")) + }, + after: func(t *testing.T) { + assert.Equal(t, true, strategy.Remove("test")) + }, + key: "test", + val: 10, + wantErr: errors.New("当前key不是float64类型"), + }, + } + + for _, tc := range testCase { + t.Run(tc.name, func(t *testing.T) { + ctx, cancelFunc := context.WithTimeout(context.Background(), time.Second*5) + defer cancelFunc() + c := NewEvictCache(strategy) + + tc.before(t) + val, err := c.IncrByFloat(ctx, tc.key, tc.val) + assert.Equal(t, tc.wantVal, val) + assert.Equal(t, tc.wantErr, err) + tc.after(t) + }) + } +} + +func newCache() (ecache.Cache, error) { + strategy := newLRUStrategy(10) + return NewEvictCache(strategy), nil +} + +func newLRUStrategy(size int) *lru.LRU[string, any] { + evictCounter := 0 + onEvicted := func(key string, value any) { + evictCounter++ + } + return lru.NewLRU[string, any]( + lru.WithCapacity[string, any](size), + lru.WithCallback[string, any](onEvicted)) +} diff --git a/cacheevict/lru/list.go b/cacheevict/lru/list.go new file mode 100644 index 0000000..eb098c9 --- /dev/null +++ b/cacheevict/lru/list.go @@ -0,0 +1,187 @@ +// Copyright 2023 ecodeclub +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package lru + +type Element[T any] struct { + Value T + list *LinkedList[T] + next, prev *Element[T] +} + +func (e *Element[T]) Next() *Element[T] { + if n := e.next; e.list != nil && n != &e.list.root { + return n + } + return nil +} + +func (e *Element[T]) Prev() *Element[T] { + if p := e.prev; e.list != nil && p != &e.list.root { + return p + } + return nil +} + +type LinkedList[T any] struct { + root Element[T] + len int +} + +func NewLinkedList[T any]() *LinkedList[T] { + l := &LinkedList[T]{} + return l.Init() +} + +func (l *LinkedList[T]) Init() *LinkedList[T] { + l.root.next = &l.root + l.root.prev = &l.root + l.len = 0 + return l +} + +func (l *LinkedList[T]) Len() int { + return l.len +} + +func (l *LinkedList[T]) Front() *Element[T] { + if l.len == 0 { + return nil + } + return l.root.next +} + +func (l *LinkedList[T]) Back() *Element[T] { + if l.len == 0 { + return nil + } + return l.root.prev +} + +func (l *LinkedList[T]) lazyInit() { + if l.root.next == nil { + l.Init() + } +} + +func (l *LinkedList[T]) insert(e, at *Element[T]) *Element[T] { + e.prev = at + e.next = at.next + e.prev.next = e + e.next.prev = e + e.list = l + l.len++ + return e +} + +func (l *LinkedList[T]) insertValue(v T, at *Element[T]) *Element[T] { + return l.insert(&Element[T]{Value: v}, at) +} + +func (l *LinkedList[T]) remove(e *Element[T]) { + e.prev.next = e.next + e.next.prev = e.prev + e.next = nil + e.prev = nil + e.list = nil + l.len-- +} + +func (l *LinkedList[T]) move(e, at *Element[T]) { + if e == at { + return + } + e.prev.next = e.next + e.next.prev = e.prev + + e.prev = at + e.next = at.next + e.prev.next = e + e.next.prev = e +} + +func (l *LinkedList[T]) Remove(e *Element[T]) any { + if e.list == l { + l.remove(e) + } + return e.Value +} + +func (l *LinkedList[T]) PushFront(v T) *Element[T] { + l.lazyInit() + return l.insertValue(v, &l.root) +} + +func (l *LinkedList[T]) PushBack(v T) *Element[T] { + l.lazyInit() + return l.insertValue(v, l.root.prev) +} + +func (l *LinkedList[T]) MoveToFront(e *Element[T]) { + if e.list != l || l.root.next == e { + return + } + l.move(e, &l.root) +} + +func (l *LinkedList[T]) MoveToBack(e *Element[T]) { + if e.list != l || l.root.prev == e { + return + } + l.move(e, l.root.prev) +} + +func (l *LinkedList[T]) MoveBefore(e, mark *Element[T]) { + if e.list != l || e == mark || mark.list != l { + return + } + l.move(e, mark.prev) +} + +func (l *LinkedList[T]) MoveAfter(e, mark *Element[T]) { + if e.list != l || e == mark || mark.list != l { + return + } + l.move(e, mark) +} + +func (l *LinkedList[T]) InsertBefore(v T, mark *Element[T]) *Element[T] { + if mark.list != l { + return nil + } + return l.insertValue(v, mark.prev) +} + +func (l *LinkedList[T]) InsertAfter(v T, mark *Element[T]) *Element[T] { + if mark.list != l { + return nil + } + return l.insertValue(v, mark) +} + +func (l *LinkedList[T]) PushBackList(other *LinkedList[T]) { + l.lazyInit() + e := other.Front() + for i := other.Len(); i > 0; i-- { + l.insertValue(e.Value, l.root.prev) + e = e.Next() + } +} + +func (l *LinkedList[T]) PushFrontList(other *LinkedList[T]) { + l.lazyInit() + for i, e := other.Len(), other.Back(); i > 0; i, e = i-1, e.Prev() { + l.insertValue(e.Value, &l.root) + } +} diff --git a/cacheevict/lru/list_test.go b/cacheevict/lru/list_test.go new file mode 100644 index 0000000..69887ab --- /dev/null +++ b/cacheevict/lru/list_test.go @@ -0,0 +1,370 @@ +// Copyright 2023 ecodeclub +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package lru + +import ( + "fmt" + "testing" +) + +func Example() { + // Create a new list and put some numbers in it. + l := NewLinkedList[int]() + e4 := l.PushBack(4) + e1 := l.PushFront(1) + l.InsertBefore(3, e4) + l.InsertAfter(2, e1) + + // Iterate through list and print its contents. + for e := l.Front(); e != nil; e = e.Next() { + fmt.Println(e.Value) + } + + // Output: + // 1 + // 2 + // 3 + // 4 +} + +func checkLinkedListLen[T any](t *testing.T, l *LinkedList[T], len int) bool { + if n := l.Len(); n != len { + t.Errorf("l.Len() = %d, want %d", n, len) + return false + } + return true +} + +func checkLinkedListPointers[T any](t *testing.T, l *LinkedList[T], es []*Element[T]) { + root := &l.root + + if !checkLinkedListLen[T](t, l, len(es)) { + return + } + + // zero length LinkLists must be the zero value or properly initialized (sentinel circle) + if len(es) == 0 { + if l.root.next != nil && l.root.next != root || l.root.prev != nil && l.root.prev != root { + t.Errorf("l.root.next = %p, l.root.prev = %p; both should both be nil or %p", l.root.next, l.root.prev, root) + } + return + } + // len(es) > 0 + + // check internal and external prev/next connections + for i, e := range es { + prev := root + Prev := (*Element[T])(nil) + if i > 0 { + prev = es[i-1] + Prev = prev + } + if p := e.prev; p != prev { + t.Errorf("elt[%d](%p).prev = %p, want %p", i, e, p, prev) + } + if p := e.Prev(); p != Prev { + t.Errorf("elt[%d](%p).Prev() = %p, want %p", i, e, p, Prev) + } + + next := root + Next := (*Element[T])(nil) + if i < len(es)-1 { + next = es[i+1] + Next = next + } + if n := e.next; n != next { + t.Errorf("elt[%d](%p).next = %p, want %p", i, e, n, next) + } + if n := e.Next(); n != Next { + t.Errorf("elt[%d](%p).Next() = %p, want %p", i, e, n, Next) + } + } +} + +func TestLinkedList(t *testing.T) { + l := NewLinkedList[any]() + checkLinkedListPointers(t, l, []*Element[any]{}) + // Single element LinkList + e := l.PushFront("a") + checkLinkedListPointers(t, l, []*Element[any]{e}) + l.MoveToFront(e) + checkLinkedListPointers(t, l, []*Element[any]{e}) + l.MoveToBack(e) + checkLinkedListPointers(t, l, []*Element[any]{e}) + l.Remove(e) + checkLinkedListPointers(t, l, []*Element[any]{}) + + // Bigger LinkList + e2 := l.PushFront(2) + e1 := l.PushFront(1) + e3 := l.PushBack(3) + e4 := l.PushBack("banana") + checkLinkedListPointers(t, l, []*Element[any]{e1, e2, e3, e4}) + + l.Remove(e2) + checkLinkedListPointers(t, l, []*Element[any]{e1, e3, e4}) + + l.MoveToFront(e3) // move from middle + checkLinkedListPointers(t, l, []*Element[any]{e3, e1, e4}) + + l.MoveToFront(e1) + l.MoveToBack(e3) // move from middle + checkLinkedListPointers(t, l, []*Element[any]{e1, e4, e3}) + + l.MoveToFront(e3) // move from back + checkLinkedListPointers(t, l, []*Element[any]{e3, e1, e4}) + l.MoveToFront(e3) // should be no-op + checkLinkedListPointers(t, l, []*Element[any]{e3, e1, e4}) + + l.MoveToBack(e3) // move from front + checkLinkedListPointers(t, l, []*Element[any]{e1, e4, e3}) + l.MoveToBack(e3) // should be no-op + checkLinkedListPointers(t, l, []*Element[any]{e1, e4, e3}) + + e2 = l.InsertBefore(2, e1) // insert before front + checkLinkedListPointers(t, l, []*Element[any]{e2, e1, e4, e3}) + l.Remove(e2) + e2 = l.InsertBefore(2, e4) // insert before middle + checkLinkedListPointers(t, l, []*Element[any]{e1, e2, e4, e3}) + l.Remove(e2) + e2 = l.InsertBefore(2, e3) // insert before back + checkLinkedListPointers(t, l, []*Element[any]{e1, e4, e2, e3}) + l.Remove(e2) + + e2 = l.InsertAfter(2, e1) // insert after front + checkLinkedListPointers(t, l, []*Element[any]{e1, e2, e4, e3}) + l.Remove(e2) + e2 = l.InsertAfter(2, e4) // insert after middle + checkLinkedListPointers(t, l, []*Element[any]{e1, e4, e2, e3}) + l.Remove(e2) + e2 = l.InsertAfter(2, e3) // insert after back + checkLinkedListPointers(t, l, []*Element[any]{e1, e4, e3, e2}) + l.Remove(e2) + + // Check standard iteration. + sum := 0 + for e := l.Front(); e != nil; e = e.Next() { + if i, ok := e.Value.(int); ok { + sum += i + } + } + if sum != 4 { + t.Errorf("sum over l = %d, want 4", sum) + } + + // Clear all elements by iterating + var next *Element[any] + for e := l.Front(); e != nil; e = next { + next = e.Next() + l.Remove(e) + } + checkLinkedListPointers(t, l, []*Element[any]{}) +} + +func checkLinkedList[T int](t *testing.T, l *LinkedList[T], es []any) { + if !checkLinkedListLen[T](t, l, len(es)) { + return + } + + i := 0 + for e := l.Front(); e != nil; e = e.Next() { + le := e.Value + if le != es[i] { + t.Errorf("elt[%d].Value = %v, want %v", i, le, es[i]) + } + i++ + } +} + +func TestExtendingEle(t *testing.T) { + l1 := NewLinkedList[int]() + l2 := NewLinkedList[int]() + + l1.PushBack(1) + l1.PushBack(2) + l1.PushBack(3) + + l2.PushBack(4) + l2.PushBack(5) + + l3 := NewLinkedList[int]() + l3.PushBackList(l1) + checkLinkedList(t, l3, []any{1, 2, 3}) + l3.PushBackList(l2) + checkLinkedList(t, l3, []any{1, 2, 3, 4, 5}) + + l3 = NewLinkedList[int]() + l3.PushFrontList(l2) + checkLinkedList(t, l3, []any{4, 5}) + l3.PushFrontList(l1) + checkLinkedList(t, l3, []any{1, 2, 3, 4, 5}) + + checkLinkedList(t, l1, []any{1, 2, 3}) + checkLinkedList(t, l2, []any{4, 5}) + + l3 = NewLinkedList[int]() + l3.PushBackList(l1) + checkLinkedList(t, l3, []any{1, 2, 3}) + l3.PushBackList(l3) + checkLinkedList(t, l3, []any{1, 2, 3, 1, 2, 3}) + + l3 = NewLinkedList[int]() + l3.PushFrontList(l1) + checkLinkedList(t, l3, []any{1, 2, 3}) + l3.PushFrontList(l3) + checkLinkedList(t, l3, []any{1, 2, 3, 1, 2, 3}) + + l3 = NewLinkedList[int]() + l1.PushBackList(l3) + checkLinkedList(t, l1, []any{1, 2, 3}) + l1.PushFrontList(l3) + checkLinkedList(t, l1, []any{1, 2, 3}) +} + +func TestRemoveEle(t *testing.T) { + l := NewLinkedList[int]() + e1 := l.PushBack(1) + e2 := l.PushBack(2) + checkLinkedListPointers(t, l, []*Element[int]{e1, e2}) + e := l.Front() + l.Remove(e) + checkLinkedListPointers(t, l, []*Element[int]{e2}) + l.Remove(e) + checkLinkedListPointers(t, l, []*Element[int]{e2}) +} + +func TestIssue4103Ele(t *testing.T) { + l1 := NewLinkedList[int]() + l1.PushBack(1) + l1.PushBack(2) + + l2 := NewLinkedList[int]() + l2.PushBack(3) + l2.PushBack(4) + + e := l1.Front() + l2.Remove(e) // l2 should not change because e is not an element of l2 + if n := l2.Len(); n != 2 { + t.Errorf("l2.Len() = %d, want 2", n) + } + + l1.InsertBefore(8, e) + if n := l1.Len(); n != 3 { + t.Errorf("l1.Len() = %d, want 3", n) + } +} + +func TestIssue6349Ele(t *testing.T) { + l := NewLinkedList[int]() + l.PushBack(1) + l.PushBack(2) + + e := l.Front() + l.Remove(e) + if e.Value != 1 { + t.Errorf("e.value = %d, want 1", e.Value) + } + if e.Next() != nil { + t.Errorf("e.Next() != nil") + } + if e.Prev() != nil { + t.Errorf("e.Prev() != nil") + } +} + +func TestMoveEle(t *testing.T) { + l := NewLinkedList[int]() + e1 := l.PushBack(1) + e2 := l.PushBack(2) + e3 := l.PushBack(3) + e4 := l.PushBack(4) + + l.MoveAfter(e3, e3) + checkLinkedListPointers(t, l, []*Element[int]{e1, e2, e3, e4}) + l.MoveBefore(e2, e2) + checkLinkedListPointers(t, l, []*Element[int]{e1, e2, e3, e4}) + + l.MoveAfter(e3, e2) + checkLinkedListPointers(t, l, []*Element[int]{e1, e2, e3, e4}) + l.MoveBefore(e2, e3) + checkLinkedListPointers(t, l, []*Element[int]{e1, e2, e3, e4}) + + l.MoveBefore(e2, e4) + checkLinkedListPointers(t, l, []*Element[int]{e1, e3, e2, e4}) + e2, e3 = e3, e2 + + l.MoveBefore(e4, e1) + checkLinkedListPointers(t, l, []*Element[int]{e4, e1, e2, e3}) + e1, e2, e3, e4 = e4, e1, e2, e3 + + l.MoveAfter(e4, e1) + checkLinkedListPointers(t, l, []*Element[int]{e1, e4, e2, e3}) + e2, e3, e4 = e4, e2, e3 + + l.MoveAfter(e2, e3) + checkLinkedListPointers(t, l, []*Element[int]{e1, e3, e2, e4}) +} + +func TestZeroLinkedList(t *testing.T) { + var l1 = new(LinkedList[int]) + l1.PushFront(1) + checkLinkedList(t, l1, []any{1}) + + var l2 = new(LinkedList[int]) + l2.PushBack(1) + checkLinkedList(t, l2, []any{1}) + + var l3 = new(LinkedList[int]) + l3.PushFrontList(l1) + checkLinkedList(t, l3, []any{1}) + + var l4 = new(LinkedList[int]) + l4.PushBackList(l2) + checkLinkedList(t, l4, []any{1}) +} + +func TestInsertBeforeUnknownMarkEle(t *testing.T) { + var l LinkedList[int] + l.PushBack(1) + l.PushBack(2) + l.PushBack(3) + l.InsertBefore(1, new(Element[int])) + checkLinkedList(t, &l, []any{1, 2, 3}) +} + +func TestInsertAfterUnknownMarkEle(t *testing.T) { + var l LinkedList[int] + l.PushBack(1) + l.PushBack(2) + l.PushBack(3) + l.InsertAfter(1, new(Element[int])) + checkLinkedList(t, &l, []any{1, 2, 3}) +} + +func TestMoveUnknownMarkEle(t *testing.T) { + var l1 LinkedList[int] + e1 := l1.PushBack(1) + + var l2 LinkedList[int] + e2 := l2.PushBack(2) + + l1.MoveAfter(e1, e2) + checkLinkedList(t, &l1, []any{1}) + checkLinkedList(t, &l2, []any{2}) + + l1.MoveBefore(e1, e2) + checkLinkedList(t, &l1, []any{1}) + checkLinkedList(t, &l2, []any{2}) +} diff --git a/cacheevict/lru/lru.go b/cacheevict/lru/lru.go new file mode 100644 index 0000000..25971d8 --- /dev/null +++ b/cacheevict/lru/lru.go @@ -0,0 +1,317 @@ +// Copyright 2023 ecodeclub +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package lru + +import ( + "sync" + "time" +) + +const ( + defaultCapacity = 100 +) + +var defaultExpiresAt = time.Time{} + +type Entry[K comparable, V any] struct { + key K + value V + expiresAt time.Time +} + +func (e Entry[K, V]) isExpired() bool { + return e.expiresAt.Before(time.Now()) +} + +func (e Entry[K, V]) existExpiration() bool { + return !e.expiresAt.Equal(defaultExpiresAt) +} + +type EvictCallback[K comparable, V any] func(key K, value V) + +type Option[K comparable, V any] func(l *LRU[K, V]) + +func WithCallback[K comparable, V any](callback func(k K, v V)) Option[K, V] { + return func(l *LRU[K, V]) { + l.callback = callback + } +} + +func WithCapacity[K comparable, V any](capacity int) Option[K, V] { + return func(l *LRU[K, V]) { + l.capacity = capacity + } +} + +type LRU[K comparable, V any] struct { + lock sync.RWMutex + capacity int + list *LinkedList[Entry[K, V]] + data map[K]*Element[Entry[K, V]] + callback EvictCallback[K, V] +} + +func NewLRU[K comparable, V any](options ...Option[K, V]) *LRU[K, V] { + res := &LRU[K, V]{ + list: NewLinkedList[Entry[K, V]](), + data: make(map[K]*Element[Entry[K, V]], 16), + capacity: defaultCapacity, + } + for _, opt := range options { + opt(res) + } + return res +} + +func (l *LRU[K, V]) Purge() { + l.lock.Lock() + defer l.lock.Unlock() + for k, v := range l.data { + if l.callback != nil { + l.callback(v.Value.key, v.Value.value) + } + l.delete(k) + } + l.list.Init() +} + +func (l *LRU[K, V]) pushEntry(key K, ent Entry[K, V]) (evicted bool) { + if elem, ok := l.data[key]; ok { + elem.Value = ent + l.list.MoveToFront(elem) + return false + } + elem := l.list.PushFront(ent) + l.data[key] = elem + evict := l.len() > l.capacity + if evict { + l.removeOldest() + } + return evict +} + +func (l *LRU[K, V]) addTTL(key K, value V, expiration time.Duration) (evicted bool) { + ent := Entry[K, V]{key: key, value: value, + expiresAt: time.Now().Add(expiration)} + return l.pushEntry(key, ent) +} + +func (l *LRU[K, V]) add(key K, value V) (evicted bool) { + ent := Entry[K, V]{key: key, value: value, + expiresAt: defaultExpiresAt} + return l.pushEntry(key, ent) +} + +func (l *LRU[K, V]) AddTTL(key K, value V, expiration time.Duration) (evicted bool) { + l.lock.Lock() + defer l.lock.Unlock() + return l.addTTL(key, value, expiration) +} + +func (l *LRU[K, V]) Add(key K, value V) (evicted bool) { + l.lock.Lock() + defer l.lock.Unlock() + return l.add(key, value) +} + +func (l *LRU[K, V]) Get(key K) (value V, ok bool) { + l.lock.Lock() + defer l.lock.Unlock() + if elem, exist := l.data[key]; exist { + entry := elem.Value + if entry.existExpiration() && entry.isExpired() { + l.removeElement(elem) + return + } + l.list.MoveToFront(elem) + return entry.value, true + } + return +} + +func (l *LRU[K, V]) peek(key K) (value V, ok bool) { + if elem, exist := l.data[key]; exist { + entry := elem.Value + if entry.existExpiration() && entry.isExpired() { + l.removeElement(elem) + return + } + return entry.value, true + } + return +} + +func (l *LRU[K, V]) Peek(key K) (value V, ok bool) { + l.lock.Lock() + defer l.lock.Unlock() + return l.peek(key) +} + +//func (l *LRU[K, V]) PeekOrAdd(key K, value V, expiration time.Duration) (V, bool, bool) { +// l.lock.Lock() +// defer l.lock.Unlock() +// val, ok := l.peek(key) +// if ok { +// return val, true, false +// } +// return val, false, l.add(key, value, expiration) +//} + +func (l *LRU[K, V]) GetOldest() (key K, value V, ok bool) { + l.lock.Lock() + defer l.lock.Unlock() + elem := l.list.Back() + for elem != nil { + entry := elem.Value + if !entry.existExpiration() || !entry.isExpired() { + return entry.key, entry.value, true + } + l.removeElement(elem) + elem = l.list.Back() + } + return +} + +func (l *LRU[K, V]) RemoveOldest() (key K, value V, ok bool) { + l.lock.Lock() + defer l.lock.Unlock() + if elem := l.list.Back(); elem != nil { + l.removeElement(elem) + return elem.Value.key, elem.Value.value, true + } + return +} + +func (l *LRU[K, V]) removeOldest() { + if ent := l.list.Back(); ent != nil { + l.removeElement(ent) + } +} + +func (l *LRU[K, V]) removeElement(elem *Element[Entry[K, V]]) { + l.list.Remove(elem) + entry := elem.Value + l.delete(entry.key) + if l.callback != nil { + l.callback(entry.key, entry.value) + } +} + +func (l *LRU[K, V]) Remove(key K) (present bool) { + l.lock.Lock() + defer l.lock.Unlock() + if elem, ok := l.data[key]; ok { + l.removeElement(elem) + if elem.Value.existExpiration() && elem.Value.isExpired() { + return false + } + return true + } + return false +} + +func (l *LRU[K, V]) Resize(size int) (evicted int) { + l.lock.Lock() + defer l.lock.Unlock() + diff := l.len() - size + if diff < 0 { + diff = 0 + } + for i := 0; i < diff; i++ { + l.removeOldest() + } + l.capacity = size + return diff +} + +func (l *LRU[K, V]) contains(key K) (ok bool) { + elem, ok := l.data[key] + if ok { + if elem.Value.existExpiration() && elem.Value.isExpired() { + l.removeElement(elem) + return false + } + } + return ok +} + +func (l *LRU[K, V]) Contains(key K) (ok bool) { + l.lock.Lock() + defer l.lock.Unlock() + return l.contains(key) +} + +//func (l *LRU[K, V]) ContainsOrAdd(key K, value V, expiration time.Duration) (ok, evicted bool) { +// l.lock.Lock() +// defer l.lock.Unlock() +// if l.contains(key) { +// return true, false +// } +// return false, l.add(key, value, expiration) +//} + +func (l *LRU[K, V]) delete(key K) { + delete(l.data, key) +} + +func (l *LRU[K, V]) len() int { + var length int + for elem := l.list.Back(); elem != nil; elem = elem.Prev() { + if elem.Value.existExpiration() && elem.Value.isExpired() { + l.removeElement(elem) + continue + } + length++ + } + return length +} + +func (l *LRU[K, V]) Len() int { + l.lock.Lock() + defer l.lock.Unlock() + return l.len() +} + +func (l *LRU[K, V]) Keys() []K { + l.lock.Lock() + defer l.lock.Unlock() + keys := make([]K, l.list.Len()) + i := 0 + for elem := l.list.Back(); elem != nil; elem = elem.Prev() { + if elem.Value.existExpiration() && elem.Value.isExpired() { + l.removeElement(elem) + continue + } + keys[i] = elem.Value.key + i++ + } + return keys +} + +func (l *LRU[K, V]) Values() []V { + l.lock.Lock() + defer l.lock.Unlock() + values := make([]V, l.list.Len()) + i := 0 + for elem := l.list.Back(); elem != nil; elem = elem.Prev() { + if elem.Value.existExpiration() && elem.Value.isExpired() { + l.removeElement(elem) + continue + } + values[i] = elem.Value.value + i++ + } + return values +} diff --git a/cacheevict/lru/lru_test.go b/cacheevict/lru/lru_test.go new file mode 100644 index 0000000..e0dda4f --- /dev/null +++ b/cacheevict/lru/lru_test.go @@ -0,0 +1,341 @@ +// Copyright 2023 ecodeclub +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package lru + +import ( + "reflect" + "testing" + "time" +) + +func TestLRUTTL(t *testing.T) { + evictCounter := 0 + onEvicted := func(k int, v int) { + if k != v { + t.Fatalf("Evict values not equal (%v!=%v)", k, v) + } + evictCounter++ + } + l := NewLRU[int, int]( + WithCapacity[int, int](128), WithCallback[int, int](onEvicted)) + + for i := 0; i < 256; i++ { + l.AddTTL(i, i, 2*time.Second) + } + if l.Len() != 128 { + t.Fatalf("bad len: %v", l.Len()) + } + + if evictCounter != 128 { + t.Fatalf("bad evict count: %v", evictCounter) + } + + for i, k := range l.Keys() { + if v, ok := l.Get(k); !ok || v != k || v != i+128 { + t.Fatalf("bad key: %v", k) + } + } + for i, v := range l.Values() { + if v != i+128 { + t.Fatalf("bad value: %v", v) + } + } + for i := 0; i < 128; i++ { + if _, ok := l.Get(i); ok { + t.Fatalf("should be evicted") + } + } + for i := 128; i < 256; i++ { + if _, ok := l.Get(i); !ok { + t.Fatalf("should not be evicted") + } + } + for i := 128; i < 192; i++ { + if ok := l.Remove(i); !ok { + t.Fatalf("should be contained") + } + if ok := l.Remove(i); ok { + t.Fatalf("should not be contained") + } + if _, ok := l.Get(i); ok { + t.Fatalf("should be deleted") + } + } + + l.Get(192) // expect 192 to be last key in l.Keys() + + for i, k := range l.Keys() { + if (i < 63 && k != i+193) || (i == 63 && k != 192) { + t.Fatalf("out of order key: %v", k) + } + } + + time.Sleep(3 * time.Second) + if l.Len() != 0 { + t.Fatalf("bad len: %v", l.Len()) + } + if _, ok := l.Get(200); ok { + t.Fatalf("should contain nothing") + } +} + +func TestLRU(t *testing.T) { + evictCounter := 0 + onEvicted := func(k int, v int) { + if k != v { + t.Fatalf("Evict values not equal (%v!=%v)", k, v) + } + evictCounter++ + } + l := NewLRU[int, int]( + WithCapacity[int, int](128), WithCallback[int, int](onEvicted)) + + for i := 0; i < 256; i++ { + l.Add(i, i) + } + if l.Len() != 128 { + t.Fatalf("bad len: %v", l.Len()) + } + + if evictCounter != 128 { + t.Fatalf("bad evict count: %v", evictCounter) + } + + for i, k := range l.Keys() { + if v, ok := l.Get(k); !ok || v != k || v != i+128 { + t.Fatalf("bad key: %v", k) + } + } + for i, v := range l.Values() { + if v != i+128 { + t.Fatalf("bad value: %v", v) + } + } + for i := 0; i < 128; i++ { + if _, ok := l.Get(i); ok { + t.Fatalf("should be evicted") + } + } + for i := 128; i < 256; i++ { + if _, ok := l.Get(i); !ok { + t.Fatalf("should not be evicted") + } + } + for i := 128; i < 192; i++ { + if ok := l.Remove(i); !ok { + t.Fatalf("should be contained") + } + if ok := l.Remove(i); ok { + t.Fatalf("should not be contained") + } + if _, ok := l.Get(i); ok { + t.Fatalf("should be deleted") + } + } + + l.Get(192) // expect 192 to be last key in l.Keys() + + for i, k := range l.Keys() { + if (i < 63 && k != i+193) || (i == 63 && k != 192) { + t.Fatalf("out of order key: %v", k) + } + } + + l.Purge() + if l.Len() != 0 { + t.Fatalf("bad len: %v", l.Len()) + } + if _, ok := l.Get(200); ok { + t.Fatalf("should contain nothing") + } +} + +func TestLRU_GetOldest_RemoveOldest(t *testing.T) { + l := NewLRU[int, int](WithCapacity[int, int](128)) + for i := 0; i < 256; i++ { + l.Add(i, i) + } + k, _, ok := l.GetOldest() + if !ok { + t.Fatalf("missing") + } + if k != 128 { + t.Fatalf("bad: %v", k) + } + + k, _, ok = l.RemoveOldest() + if !ok { + t.Fatalf("missing") + } + if k != 128 { + t.Fatalf("bad: %v", k) + } + + k, _, ok = l.RemoveOldest() + if !ok { + t.Fatalf("missing") + } + if k != 129 { + t.Fatalf("bad: %v", k) + } +} + +// Test that Add returns true/false if an eviction occurred +func TestLRU_Add(t *testing.T) { + evictCounter := 0 + onEvicted := func(k int, v int) { + evictCounter++ + } + + l := NewLRU[int, int]( + WithCapacity[int, int](1), WithCallback[int, int](onEvicted)) + + if l.Add(1, 1) == true || evictCounter != 0 { + t.Errorf("should not have an eviction") + } + if l.Add(2, 2) == false || evictCounter != 1 { + t.Errorf("should have an eviction") + } +} + +// Test that Contains doesn't update recent-ness +func TestLRU_Contains(t *testing.T) { + l := NewLRU[int, int](WithCapacity[int, int](2)) + + l.Add(1, 1) + l.Add(2, 2) + if !l.Contains(1) { + t.Errorf("1 should be contained") + } + + l.Add(3, 3) + if l.Contains(1) { + t.Errorf("Contains should not have updated recent-ness of 1") + } + + l.AddTTL(4, 4, time.Second) + if !l.Contains(4) { + t.Errorf("4 should be contained") + } + time.Sleep(time.Second) + if l.Contains(4) { + t.Errorf("Contains should not have updated recent-ness of 4") + } +} + +// Test that Peek doesn't update recent-ness +func TestLRU_Peek(t *testing.T) { + l := NewLRU[int, int](WithCapacity[int, int](2)) + + l.Add(1, 1) + l.Add(2, 2) + if v, ok := l.Peek(1); !ok || v != 1 { + t.Errorf("1 should be set to 1: %v, %v", v, ok) + } + + l.Add(3, 3) + if l.Contains(1) { + t.Errorf("should not have updated recent-ness of 1") + } + + l.AddTTL(4, 4, time.Second) + if v, ok := l.Peek(4); !ok || v != 4 { + t.Errorf("1 should be set to 1: %v, %v", v, ok) + } + time.Sleep(time.Second) + if l.Contains(4) { + t.Errorf("Contains should not have updated recent-ness of 4") + } +} + +// Test that Resize can upsize and downsize +func TestLRU_Resize(t *testing.T) { + onEvictCounter := 0 + onEvicted := func(k int, v int) { + onEvictCounter++ + } + l := NewLRU[int, int]( + WithCapacity[int, int](2), WithCallback[int, int](onEvicted)) + + // Downsize + l.Add(1, 1) + l.Add(2, 2) + evicted := l.Resize(1) + if evicted != 1 { + t.Errorf("1 element should have been evicted: %v", evicted) + } + if onEvictCounter != 1 { + t.Errorf("onEvicted should have been called 1 time: %v", onEvictCounter) + } + + l.Add(3, 3) + if l.Contains(1) { + t.Errorf("Element 1 should have been evicted") + } + + // Upsize + evicted = l.Resize(2) + if evicted != 0 { + t.Errorf("0 elements should have been evicted: %v", evicted) + } + + l.Add(4, 4) + if !l.Contains(3) || !l.Contains(4) { + t.Errorf("Cache should have contained 2 elements") + } +} + +func (l *LRU[K, V]) wantKeys(t *testing.T, want []K) { + t.Helper() + got := l.Keys() + if !reflect.DeepEqual(got, want) { + t.Errorf("wrong keys got: %v, want: %v ", got, want) + } +} + +func TestCache_EvictionSameKey(t *testing.T) { + var evictedKeys []int + onEvicted := func(key int, _ struct{}) { + evictedKeys = append(evictedKeys, key) + } + + l := NewLRU[int, struct{}]( + WithCapacity[int, struct{}](2), WithCallback[int, struct{}](onEvicted)) + + if evicted := l.Add(1, struct{}{}); evicted { + t.Error("First 1: got unexpected eviction") + } + l.wantKeys(t, []int{1}) + + if evicted := l.Add(2, struct{}{}); evicted { + t.Error("2: got unexpected eviction") + } + l.wantKeys(t, []int{1, 2}) + + if evicted := l.Add(1, struct{}{}); evicted { + t.Error("Second 1: got unexpected eviction") + } + l.wantKeys(t, []int{2, 1}) + + if evicted := l.Add(3, struct{}{}); !evicted { + t.Error("3: did not get expected eviction") + } + l.wantKeys(t, []int{1, 3}) + + want := []int{2} + if !reflect.DeepEqual(evictedKeys, want) { + t.Errorf("evictedKeys got: %v want: %v", evictedKeys, want) + } +} diff --git a/cacheevict/types.go b/cacheevict/types.go new file mode 100644 index 0000000..37b02e5 --- /dev/null +++ b/cacheevict/types.go @@ -0,0 +1,39 @@ +// Copyright 2023 ecodeclub +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cacheevict + +import ( + "time" + + "github.com/ecodeclub/ecache/cacheevict/lru" +) + +var _ EvictStrategy[string, any] = (*lru.LRU[string, any])(nil) + +type EvictStrategy[K comparable, V any] interface { + AddTTL(key K, value V, expiration time.Duration) bool + Add(key K, value V) bool + Get(key K) (value V, ok bool) + Contains(key K) (ok bool) + Peek(key K) (value V, ok bool) + Remove(key K) bool + RemoveOldest() (K, V, bool) + GetOldest() (K, V, bool) + Keys() []K + Values() []V + Len() int + Purge() + Resize(int) int +} From 62bb38b545f7c9770e99bcea512176851d7514ab Mon Sep 17 00:00:00 2001 From: Stone-afk <1711865140@qq.com> Date: Sat, 23 Dec 2023 12:34:32 +0800 Subject: [PATCH 02/16] feature add lru cache evict --- cacheevict/lru/lru.go | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/cacheevict/lru/lru.go b/cacheevict/lru/lru.go index 25971d8..72f1bcc 100644 --- a/cacheevict/lru/lru.go +++ b/cacheevict/lru/lru.go @@ -159,16 +159,6 @@ func (l *LRU[K, V]) Peek(key K) (value V, ok bool) { return l.peek(key) } -//func (l *LRU[K, V]) PeekOrAdd(key K, value V, expiration time.Duration) (V, bool, bool) { -// l.lock.Lock() -// defer l.lock.Unlock() -// val, ok := l.peek(key) -// if ok { -// return val, true, false -// } -// return val, false, l.add(key, value, expiration) -//} - func (l *LRU[K, V]) GetOldest() (key K, value V, ok bool) { l.lock.Lock() defer l.lock.Unlock() @@ -253,15 +243,6 @@ func (l *LRU[K, V]) Contains(key K) (ok bool) { return l.contains(key) } -//func (l *LRU[K, V]) ContainsOrAdd(key K, value V, expiration time.Duration) (ok, evicted bool) { -// l.lock.Lock() -// defer l.lock.Unlock() -// if l.contains(key) { -// return true, false -// } -// return false, l.add(key, value, expiration) -//} - func (l *LRU[K, V]) delete(key K) { delete(l.data, key) } From ce97d517425713825fa3e1a8ac0de7a7511b5999 Mon Sep 17 00:00:00 2001 From: Stone-afk <1711865140@qq.com> Date: Sun, 24 Dec 2023 17:23:47 +0800 Subject: [PATCH 03/16] feature add lru cache evict --- cacheevict/cache.go | 317 -------- cacheevict/cache_test.go | 983 ------------------------ cacheevict/types.go | 39 - memory/lru/cache.go | 63 +- memory/lru/cache_test.go | 269 ++++--- {cacheevict => memory}/lru/list.go | 0 {cacheevict => memory}/lru/list_test.go | 0 {cacheevict => memory}/lru/lru.go | 27 +- {cacheevict => memory}/lru/lru_test.go | 0 9 files changed, 212 insertions(+), 1486 deletions(-) delete mode 100644 cacheevict/cache.go delete mode 100644 cacheevict/cache_test.go delete mode 100644 cacheevict/types.go rename {cacheevict => memory}/lru/list.go (100%) rename {cacheevict => memory}/lru/list_test.go (100%) rename {cacheevict => memory}/lru/lru.go (90%) rename {cacheevict => memory}/lru/lru_test.go (100%) diff --git a/cacheevict/cache.go b/cacheevict/cache.go deleted file mode 100644 index ed240cc..0000000 --- a/cacheevict/cache.go +++ /dev/null @@ -1,317 +0,0 @@ -// Copyright 2023 ecodeclub -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package cacheevict - -import ( - "context" - "errors" - "fmt" - "sync" - "time" - - "github.com/ecodeclub/ecache" - "github.com/ecodeclub/ecache/internal/errs" - "github.com/ecodeclub/ekit/list" - "github.com/ecodeclub/ekit/set" -) - -var ( - _ ecache.Cache = (*EvictCache)(nil) -) - -type Option func(l *EvictCache) - -func WithEvictStrategy(strategy EvictStrategy[string, any]) Option { - return func(l *EvictCache) { - l.strategy = strategy - } -} - -type EvictCache struct { - lock sync.RWMutex - strategy EvictStrategy[string, any] -} - -func NewEvictCache(strategy EvictStrategy[string, any]) *EvictCache { - return &EvictCache{ - strategy: strategy, - } -} - -func (c *EvictCache) Set(ctx context.Context, key string, val any, expiration time.Duration) error { - c.lock.Lock() - defer c.lock.Unlock() - c.strategy.AddTTL(key, val, expiration) - return nil -} - -// SetNX 由 strategy 统一控制过期时间 -func (c *EvictCache) SetNX(ctx context.Context, key string, val any, expiration time.Duration) (bool, error) { - c.lock.Lock() - defer c.lock.Unlock() - - if c.strategy.Contains(key) { - return false, nil - } - - c.strategy.AddTTL(key, val, expiration) - - return true, nil -} - -func (c *EvictCache) Get(ctx context.Context, key string) (val ecache.Value) { - c.lock.Lock() - defer c.lock.Unlock() - var ok bool - val.Val, ok = c.strategy.Get(key) - if !ok { - val.Err = errs.ErrKeyNotExist - } - - return -} - -func (c *EvictCache) GetSet(ctx context.Context, key string, val string) (result ecache.Value) { - c.lock.Lock() - defer c.lock.Unlock() - - var ok bool - result.Val, ok = c.strategy.Get(key) - if !ok { - result.Err = errs.ErrKeyNotExist - } - - c.strategy.Add(key, val) - - return -} - -func (c *EvictCache) Delete(ctx context.Context, key ...string) (int64, error) { - c.lock.Lock() - defer c.lock.Unlock() - - n := int64(0) - for _, k := range key { - if ctx.Err() != nil { - return n, ctx.Err() - } - _, ok := c.strategy.Get(k) - if !ok { - continue - } - if c.strategy.Remove(k) { - n++ - } else { - return n, fmt.Errorf("%w: key = %s", errs.ErrDeleteKeyFailed, k) - } - } - return n, nil -} - -// anySliceToValueSlice 公共转换 -func (c *EvictCache) anySliceToValueSlice(data ...any) []ecache.Value { - newVal := make([]ecache.Value, len(data), cap(data)) - for key, value := range data { - anyVal := ecache.Value{} - anyVal.Val = value - newVal[key] = anyVal - } - return newVal -} - -func (c *EvictCache) LPush(ctx context.Context, key string, val ...any) (int64, error) { - c.lock.Lock() - defer c.lock.Unlock() - - var ( - ok bool - result = ecache.Value{} - ) - result.Val, ok = c.strategy.Get(key) - if !ok { - l := &list.ConcurrentList[ecache.Value]{ - List: list.NewLinkedListOf[ecache.Value](c.anySliceToValueSlice(val...)), - } - c.strategy.Add(key, l) - return int64(l.Len()), nil - } - - data, ok := result.Val.(list.List[ecache.Value]) - if !ok { - return 0, errors.New("当前key不是list类型") - } - - err := data.Append(c.anySliceToValueSlice(val)...) - if err != nil { - return 0, err - } - - c.strategy.Add(key, data) - return int64(data.Len()), nil -} - -func (c *EvictCache) LPop(ctx context.Context, key string) (val ecache.Value) { - c.lock.Lock() - defer c.lock.Unlock() - - var ( - ok bool - ) - val.Val, ok = c.strategy.Get(key) - if !ok { - val.Err = errs.ErrKeyNotExist - return - } - - data, ok := val.Val.(list.List[ecache.Value]) - if !ok { - val.Err = errors.New("当前key不是list类型") - return - } - - value, err := data.Delete(0) - if err != nil { - val.Err = err - return - } - - val = value - return -} - -func (c *EvictCache) SAdd(ctx context.Context, key string, members ...any) (int64, error) { - c.lock.Lock() - defer c.lock.Unlock() - - var ( - ok bool - result = ecache.Value{} - ) - result.Val, ok = c.strategy.Get(key) - if !ok { - result.Val = set.NewMapSet[any](8) - } - - s, ok := result.Val.(set.Set[any]) - if !ok { - return 0, errors.New("当前key已存在不是set类型") - } - - for _, value := range members { - s.Add(value) - } - c.strategy.Add(key, s) - - return int64(len(s.Keys())), nil -} - -func (c *EvictCache) SRem(ctx context.Context, key string, members ...any) (int64, error) { - c.lock.Lock() - defer c.lock.Unlock() - - result, ok := c.strategy.Get(key) - if !ok { - return 0, errs.ErrKeyNotExist - } - - s, ok := result.(set.Set[any]) - if !ok { - return 0, errors.New("当前key已存在不是set类型") - } - - var rems int64 - for _, member := range members { - if s.Exist(member) { - s.Delete(member) - rems++ - } - } - return rems, nil -} - -func (c *EvictCache) IncrBy(ctx context.Context, key string, value int64) (int64, error) { - c.lock.Lock() - defer c.lock.Unlock() - - var ( - ok bool - result = ecache.Value{} - ) - result.Val, ok = c.strategy.Get(key) - if !ok { - c.strategy.Add(key, value) - return value, nil - } - - incr, err := result.Int64() - if err != nil { - return 0, errors.New("当前key不是int64类型") - } - - newVal := incr + value - c.strategy.Add(key, newVal) - - return newVal, nil -} - -func (c *EvictCache) DecrBy(ctx context.Context, key string, value int64) (int64, error) { - c.lock.Lock() - defer c.lock.Unlock() - - var ( - ok bool - result = ecache.Value{} - ) - result.Val, ok = c.strategy.Get(key) - if !ok { - c.strategy.Add(key, -value) - return -value, nil - } - - decr, err := result.Int64() - if err != nil { - return 0, errors.New("当前key不是int64类型") - } - - newVal := decr - value - c.strategy.Add(key, newVal) - - return newVal, nil -} - -func (c *EvictCache) IncrByFloat(ctx context.Context, key string, value float64) (float64, error) { - c.lock.Lock() - defer c.lock.Unlock() - - var ( - ok bool - result = ecache.Value{} - ) - result.Val, ok = c.strategy.Get(key) - if !ok { - c.strategy.Add(key, value) - return value, nil - } - - val, err := result.Float64() - if err != nil { - return 0, errors.New("当前key不是float64类型") - } - - newVal := val + value - c.strategy.Add(key, newVal) - - return newVal, nil -} diff --git a/cacheevict/cache_test.go b/cacheevict/cache_test.go deleted file mode 100644 index 44e2e4f..0000000 --- a/cacheevict/cache_test.go +++ /dev/null @@ -1,983 +0,0 @@ -// Copyright 2023 ecodeclub -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package cacheevict - -import ( - "context" - "errors" - "testing" - "time" - - "github.com/ecodeclub/ecache" - "github.com/ecodeclub/ecache/cacheevict/lru" - "github.com/ecodeclub/ecache/internal/errs" - "github.com/ecodeclub/ekit/list" - "github.com/ecodeclub/ekit/set" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestEvictCache_Set(t *testing.T) { - evictCounter := 0 - onEvicted := func(key string, value any) { - evictCounter++ - } - strategy := lru.NewLRU[string, any]( - lru.WithCapacity[string, any](5), - lru.WithCallback[string, any](onEvicted)) - - testCase := []struct { - name string - after func(t *testing.T) - - key string - val string - expiration time.Duration - - wantErr error - }{ - { - name: "set value", - after: func(t *testing.T) { - result, ok := strategy.Get("test") - assert.Equal(t, true, ok) - assert.Equal(t, "hello ecache", result.(string)) - assert.Equal(t, true, strategy.Remove("test")) - }, - key: "test", - val: "hello ecache", - expiration: time.Minute, - }, - } - - for _, tc := range testCase { - t.Run(tc.name, func(t *testing.T) { - ctx, cancelFunc := context.WithTimeout(context.Background(), time.Second*5) - defer cancelFunc() - c := NewEvictCache(strategy) - - err := c.Set(ctx, tc.key, tc.val, tc.expiration) - require.NoError(t, err) - tc.after(t) - }) - } -} - -func TestEvictCache_Get(t *testing.T) { - evictCounter := 0 - onEvicted := func(key string, value any) { - evictCounter++ - } - strategy := lru.NewLRU[string, any]( - lru.WithCapacity[string, any](5), - lru.WithCallback[string, any](onEvicted)) - - testCase := []struct { - name string - before func(t *testing.T) - after func(t *testing.T) - - key string - - wantVal string - wantErr error - }{ - { - name: "get value", - before: func(t *testing.T) { - assert.Equal(t, false, strategy.Add("test", "hello ecache")) - }, - after: func(t *testing.T) { - assert.Equal(t, true, strategy.Remove("test")) - }, - key: "test", - wantVal: "hello ecache", - }, - { - name: "get expired value", - before: func(t *testing.T) { - assert.Equal(t, false, - strategy.AddTTL("test", "hello ecache", time.Second)) - }, - after: func(t *testing.T) { - time.Sleep(time.Second) - _, ok := strategy.Get("test") - assert.Equal(t, false, ok) - }, - key: "test", - wantVal: "hello ecache", - }, - { - name: "get value err", - before: func(t *testing.T) {}, - after: func(t *testing.T) {}, - key: "test", - wantErr: errs.ErrKeyNotExist, - }, - } - - for _, tc := range testCase { - t.Run(tc.name, func(t *testing.T) { - ctx, cancelFunc := context.WithTimeout(context.Background(), time.Second*5) - defer cancelFunc() - c := NewEvictCache(strategy) - - tc.before(t) - result := c.Get(ctx, tc.key) - val, err := result.String() - assert.Equal(t, tc.wantVal, val) - assert.Equal(t, tc.wantErr, err) - tc.after(t) - }) - } -} - -func TestEvictCache_SetNX(t *testing.T) { - evictCounter := 0 - onEvicted := func(key string, value any) { - evictCounter++ - } - strategy := lru.NewLRU[string, any]( - lru.WithCapacity[string, any](1), - lru.WithCallback[string, any](onEvicted)) - - testCase := []struct { - name string - before func(t *testing.T) - after func(t *testing.T) - - key string - val string - expire time.Duration - wantVal bool - }{ - { - name: "setnx value", - before: func(t *testing.T) {}, - after: func(t *testing.T) {}, - key: "test", - val: "hello ecache", - expire: time.Minute, - wantVal: true, - }, - { - name: "setnx value exist", - before: func(t *testing.T) { - assert.Equal(t, false, strategy.Add("test", "hello ecache")) - }, - after: func(t *testing.T) { - assert.Equal(t, true, strategy.Remove("test")) - }, - key: "test", - val: "hello world", - expire: time.Minute, - wantVal: false, - }, - { - name: "setnx expired value", - before: func(t *testing.T) { - assert.Equal(t, false, strategy.AddTTL("test", "hello ecache", time.Second)) - }, - after: func(t *testing.T) { - time.Sleep(time.Second) - assert.Equal(t, false, strategy.Remove("test")) - }, - key: "test", - val: "hello world", - expire: time.Minute, - wantVal: false, - }, - } - - for _, tc := range testCase { - t.Run(tc.name, func(t *testing.T) { - ctx, cancelFunc := context.WithTimeout(context.Background(), time.Second*5) - defer cancelFunc() - c := NewEvictCache(strategy) - - tc.before(t) - result, err := c.SetNX(ctx, tc.key, tc.val, tc.expire) - assert.Equal(t, tc.wantVal, result) - require.NoError(t, err) - tc.after(t) - }) - } -} - -func TestEvictCache_GetSet(t *testing.T) { - evictCounter := 0 - onEvicted := func(key string, value any) { - evictCounter++ - } - strategy := lru.NewLRU[string, any]( - lru.WithCapacity[string, any](5), - lru.WithCallback[string, any](onEvicted)) - - testCase := []struct { - name string - before func(t *testing.T) - after func(t *testing.T) - - key string - val string - wantVal string - wantErr error - }{ - { - name: "getset value", - before: func(t *testing.T) { - assert.Equal(t, false, strategy.Add("test", "hello ecache")) - }, - after: func(t *testing.T) { - result, ok := strategy.Get("test") - assert.Equal(t, true, ok) - assert.Equal(t, "hello world", result) - assert.Equal(t, true, strategy.Remove("test")) - }, - key: "test", - val: "hello world", - wantVal: "hello ecache", - }, - { - name: "getset value not key error", - before: func(t *testing.T) {}, - after: func(t *testing.T) { - result, ok := strategy.Get("test") - assert.Equal(t, true, ok) - assert.Equal(t, "hello world", result) - assert.Equal(t, true, strategy.Remove("test")) - }, - key: "test", - val: "hello world", - wantErr: errs.ErrKeyNotExist, - }, - } - - for _, tc := range testCase { - t.Run(tc.name, func(t *testing.T) { - ctx, cancelFunc := context.WithTimeout(context.Background(), time.Second*5) - defer cancelFunc() - c := NewEvictCache(strategy) - - tc.before(t) - result := c.GetSet(ctx, tc.key, tc.val) - val, err := result.String() - assert.Equal(t, tc.wantVal, val) - assert.Equal(t, tc.wantErr, err) - tc.after(t) - }) - } -} - -func TestEvictCache_Delete(t *testing.T) { - cache, err := newCache() - require.NoError(t, err) - - testCases := []struct { - name string - before func(ctx context.Context, t *testing.T, cache ecache.Cache) - - ctxFunc func() context.Context - key []string - - wantN int64 - wantErr error - }{ - { - name: "delete expired key", - before: func(ctx context.Context, t *testing.T, cache ecache.Cache) { - require.NoError(t, cache.Set(ctx, "name", "Alex", 0)) - }, - ctxFunc: func() context.Context { - return context.Background() - }, - key: []string{"name"}, - wantN: 0, - }, - { - name: "delete single existed key", - before: func(ctx context.Context, t *testing.T, cache ecache.Cache) { - require.NoError(t, cache.Set(ctx, "name", "Alex", 10*time.Second)) - }, - ctxFunc: func() context.Context { - return context.Background() - }, - key: []string{"name"}, - wantN: 1, - }, - { - name: "delete single does not existed key", - before: func(ctx context.Context, t *testing.T, cache ecache.Cache) {}, - ctxFunc: func() context.Context { - return context.Background() - }, - key: []string{"notExistedKey"}, - }, - { - name: "delete multiple expired keys", - before: func(ctx context.Context, t *testing.T, cache ecache.Cache) { - require.NoError(t, cache.Set(ctx, "name", "Alex", 0)) - require.NoError(t, cache.Set(ctx, "age", 18, 0)) - }, - ctxFunc: func() context.Context { - return context.Background() - }, - key: []string{"name", "age"}, - wantN: 0, - }, - { - name: "delete multiple existed keys", - before: func(ctx context.Context, t *testing.T, cache ecache.Cache) { - require.NoError(t, cache.Set(ctx, "name", "Alex", 10*time.Second)) - require.NoError(t, cache.Set(ctx, "age", 18, 10*time.Second)) - }, - ctxFunc: func() context.Context { - return context.Background() - }, - key: []string{"name", "age"}, - wantN: 2, - }, - { - name: "delete multiple do not existed keys", - before: func(ctx context.Context, t *testing.T, cache ecache.Cache) {}, - ctxFunc: func() context.Context { - return context.Background() - }, - key: []string{"name", "age"}, - }, - { - name: "delete multiple keys, some do not expired keys", - before: func(ctx context.Context, t *testing.T, cache ecache.Cache) { - require.NoError(t, cache.Set(ctx, "name", "Alex", 0)) - require.NoError(t, cache.Set(ctx, "age", 18, 0)) - require.NoError(t, cache.Set(ctx, "gender", "male", 0)) - }, - ctxFunc: func() context.Context { - return context.Background() - }, - key: []string{"name", "age", "gender", "addr"}, - wantN: 0, - }, - { - name: "delete multiple keys, some do not existed keys", - before: func(ctx context.Context, t *testing.T, cache ecache.Cache) { - require.NoError(t, cache.Set(ctx, "name", "Alex", 10*time.Second)) - require.NoError(t, cache.Set(ctx, "age", 18, 10*time.Second)) - require.NoError(t, cache.Set(ctx, "gender", "male", 10*time.Second)) - }, - ctxFunc: func() context.Context { - return context.Background() - }, - key: []string{"name", "age", "gender", "addr"}, - wantN: 3, - }, - { - name: "timeout", - before: func(ctx context.Context, t *testing.T, cache ecache.Cache) {}, - ctxFunc: func() context.Context { - timeout := time.Millisecond * 100 - ctx, cancel := context.WithTimeout(context.Background(), timeout) - defer cancel() - time.Sleep(timeout * 2) - return ctx - }, - key: []string{"name", "age", "addr"}, - wantErr: context.DeadlineExceeded, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - ctx := tc.ctxFunc() - tc.before(ctx, t, cache) - n, err := cache.Delete(ctx, tc.key...) - if err != nil { - assert.ErrorIs(t, err, tc.wantErr) - return - } - assert.Equal(t, tc.wantN, n) - }) - } -} - -func TestEvictCache_LPush(t *testing.T) { - evictCounter := 0 - onEvicted := func(key string, value any) { - evictCounter++ - } - strategy := lru.NewLRU[string, any]( - lru.WithCapacity[string, any](5), - lru.WithCallback[string, any](onEvicted)) - - testCase := []struct { - name string - before func(t *testing.T) - after func(t *testing.T) - - key string - val []any - wantVal int64 - wantErr error - }{ - { - name: "lpush value", - before: func(t *testing.T) {}, - after: func(t *testing.T) { - assert.Equal(t, true, strategy.Remove("test")) - }, - key: "test", - val: []any{"hello ecache"}, - wantVal: 1, - }, - { - name: "lpush multiple value", - before: func(t *testing.T) {}, - after: func(t *testing.T) { - assert.Equal(t, true, strategy.Remove("test")) - }, - key: "test", - val: []any{"hello ecache", "hello world"}, - wantVal: 2, - }, - { - name: "lpush value exists", - before: func(t *testing.T) { - val := ecache.Value{} - val.Val = "hello ecache" - l := &list.ConcurrentList[ecache.Value]{ - List: list.NewLinkedListOf[ecache.Value]([]ecache.Value{val}), - } - assert.Equal(t, false, strategy.Add("test", l)) - }, - after: func(t *testing.T) { - assert.Equal(t, true, strategy.Remove("test")) - }, - key: "test", - val: []any{"hello world"}, - wantVal: 2, - }, - { - name: "lpush value not type", - before: func(t *testing.T) { - assert.Equal(t, false, strategy.Add("test", "string")) - }, - after: func(t *testing.T) { - assert.Equal(t, true, strategy.Remove("test")) - }, - key: "test", - val: []any{"hello ecache"}, - wantErr: errors.New("当前key不是list类型"), - }, - } - - for _, tc := range testCase { - t.Run(tc.name, func(t *testing.T) { - ctx, cancelFunc := context.WithTimeout(context.Background(), time.Second*5) - defer cancelFunc() - c := NewEvictCache(strategy) - - tc.before(t) - length, err := c.LPush(ctx, tc.key, tc.val...) - assert.Equal(t, tc.wantVal, length) - assert.Equal(t, tc.wantErr, err) - tc.after(t) - }) - } -} - -func TestEvictCache_LPop(t *testing.T) { - evictCounter := 0 - onEvicted := func(key string, value any) { - evictCounter++ - } - strategy := lru.NewLRU[string, any]( - lru.WithCapacity[string, any](5), - lru.WithCallback[string, any](onEvicted)) - - testCase := []struct { - name string - before func(t *testing.T) - after func(t *testing.T) - - key string - wantVal string - wantErr error - }{ - { - name: "lpop value", - before: func(t *testing.T) { - val := ecache.Value{} - val.Val = "hello ecache" - l := &list.ConcurrentList[ecache.Value]{ - List: list.NewLinkedListOf[ecache.Value]([]ecache.Value{val}), - } - assert.Equal(t, false, strategy.Add("test", l)) - }, - after: func(t *testing.T) { - assert.Equal(t, true, strategy.Remove("test")) - }, - key: "test", - wantVal: "hello ecache", - }, - { - name: "lpop value not nil", - before: func(t *testing.T) { - val := ecache.Value{} - val.Val = "hello ecache" - val2 := ecache.Value{} - val2.Val = "hello world" - l := &list.ConcurrentList[ecache.Value]{ - List: list.NewLinkedListOf[ecache.Value]([]ecache.Value{val, val2}), - } - assert.Equal(t, false, strategy.Add("test", l)) - }, - after: func(t *testing.T) { - val, ok := strategy.Get("test") - assert.Equal(t, true, ok) - result, ok := val.(list.List[ecache.Value]) - assert.Equal(t, true, ok) - assert.Equal(t, 1, result.Len()) - value, err := result.Delete(0) - assert.NoError(t, err) - assert.Equal(t, "hello world", value.Val) - assert.NoError(t, value.Err) - - assert.Equal(t, true, strategy.Remove("test")) - }, - key: "test", - wantVal: "hello ecache", - }, - { - name: "lpop value type error", - before: func(t *testing.T) { - assert.Equal(t, false, strategy.Add("test", "hello world")) - }, - after: func(t *testing.T) { - assert.Equal(t, true, strategy.Remove("test")) - }, - key: "test", - wantErr: errors.New("当前key不是list类型"), - }, - { - name: "lpop not key", - before: func(t *testing.T) {}, - after: func(t *testing.T) {}, - key: "test", - wantErr: errs.ErrKeyNotExist, - }, - } - - for _, tc := range testCase { - t.Run(tc.name, func(t *testing.T) { - ctx, cancelFunc := context.WithTimeout(context.Background(), time.Second*5) - defer cancelFunc() - c := NewEvictCache(strategy) - - tc.before(t) - val := c.LPop(ctx, tc.key) - result, err := val.String() - assert.Equal(t, tc.wantVal, result) - assert.Equal(t, tc.wantErr, err) - tc.after(t) - }) - } -} - -func TestEvictCache_SAdd(t *testing.T) { - evictCounter := 0 - onEvicted := func(key string, value any) { - evictCounter++ - } - strategy := lru.NewLRU[string, any]( - lru.WithCapacity[string, any](5), - lru.WithCallback[string, any](onEvicted)) - - testCase := []struct { - name string - before func(t *testing.T) - after func(t *testing.T) - - key string - val []any - wantVal int64 - wantErr error - }{ - { - name: "sadd value", - before: func(t *testing.T) {}, - after: func(t *testing.T) { - assert.Equal(t, true, strategy.Remove("test")) - }, - key: "test", - val: []any{"hello ecache", "hello world"}, - wantVal: 2, - }, - { - name: "sadd value exist", - before: func(t *testing.T) { - s := set.NewMapSet[any](8) - s.Add("hello world") - - assert.Equal(t, false, strategy.Add("test", s)) - }, - after: func(t *testing.T) { - assert.Equal(t, true, strategy.Remove("test")) - }, - key: "test", - val: []any{"hello ecache"}, - wantVal: 2, - }, - { - name: "sadd value type err", - before: func(t *testing.T) { - assert.Equal(t, false, strategy.Add("test", "string")) - }, - after: func(t *testing.T) { - assert.Equal(t, true, strategy.Remove("test")) - }, - key: "test", - val: []any{"hello"}, - wantErr: errors.New("当前key已存在不是set类型"), - }, - } - - for _, tc := range testCase { - t.Run(tc.name, func(t *testing.T) { - ctx, cancelFunc := context.WithTimeout(context.Background(), time.Second*5) - defer cancelFunc() - c := NewEvictCache(strategy) - - tc.before(t) - val, err := c.SAdd(ctx, tc.key, tc.val...) - assert.Equal(t, tc.wantVal, val) - assert.Equal(t, tc.wantErr, err) - tc.after(t) - }) - } -} - -func TestCache_SRem(t *testing.T) { - evictCounter := 0 - onEvicted := func(key string, value any) { - evictCounter++ - } - strategy := lru.NewLRU[string, any]( - lru.WithCapacity[string, any](5), - lru.WithCallback[string, any](onEvicted)) - - testCase := []struct { - name string - before func(t *testing.T) - after func(t *testing.T) - - key string - val []any - - wantVal int64 - wantErr error - }{ - { - name: "srem value", - before: func(t *testing.T) { - s := set.NewMapSet[any](8) - - s.Add("hello world") - s.Add("hello ecache") - - assert.Equal(t, false, strategy.Add("test", s)) - }, - after: func(t *testing.T) { - assert.Equal(t, true, strategy.Remove("test")) - }, - key: "test", - val: []any{"hello world"}, - wantVal: 1, - }, - { - name: "srem value ignore", - before: func(t *testing.T) { - s := set.NewMapSet[any](8) - s.Add("hello world") - - assert.Equal(t, false, strategy.Add("test", s)) - }, - after: func(t *testing.T) { - assert.Equal(t, true, strategy.Remove("test")) - }, - key: "test", - val: []any{"hello ecache"}, - wantVal: 0, - }, - { - name: "srem value nil", - before: func(t *testing.T) {}, - after: func(t *testing.T) {}, - key: "test", - val: []any{"hello world"}, - wantErr: errs.ErrKeyNotExist, - }, - { - name: "srem value type error", - before: func(t *testing.T) { - assert.Equal(t, false, strategy.Add("test", int64(1))) - }, - after: func(t *testing.T) { - assert.Equal(t, true, strategy.Remove("test")) - }, - key: "test", - val: []any{"hello world"}, - wantErr: errors.New("当前key已存在不是set类型"), - }, - } - - for _, tc := range testCase { - t.Run(tc.name, func(t *testing.T) { - defer tc.after(t) - ctx, cancelFunc := context.WithTimeout(context.Background(), time.Second*5) - defer cancelFunc() - c := NewEvictCache(strategy) - - tc.before(t) - val, err := c.SRem(ctx, tc.key, tc.val...) - assert.Equal(t, tc.wantErr, err) - assert.Equal(t, tc.wantVal, val) - }) - } -} - -func TestEvictCache_IncrBy(t *testing.T) { - evictCounter := 0 - onEvicted := func(key string, value any) { - evictCounter++ - } - strategy := lru.NewLRU[string, any]( - lru.WithCapacity[string, any](5), - lru.WithCallback[string, any](onEvicted)) - - testCase := []struct { - name string - before func(t *testing.T) - after func(t *testing.T) - - key string - val int64 - wantVal int64 - wantErr error - }{ - { - name: "incrby value", - before: func(t *testing.T) {}, - after: func(t *testing.T) { - assert.Equal(t, true, strategy.Remove("test")) - }, - key: "test", - val: 1, - wantVal: 1, - }, - { - name: "incrby value add", - before: func(t *testing.T) { - assert.Equal(t, false, strategy.Add("test", int64(1))) - }, - after: func(t *testing.T) { - assert.Equal(t, true, strategy.Remove("test")) - }, - key: "test", - val: 1, - wantVal: 2, - }, - { - name: "incrby value type error", - before: func(t *testing.T) { - assert.Equal(t, false, strategy.Add("test", 12.62)) - }, - after: func(t *testing.T) { - assert.Equal(t, true, strategy.Remove("test")) - }, - key: "test", - val: 1, - wantErr: errors.New("当前key不是int64类型"), - }, - } - - for _, tc := range testCase { - t.Run(tc.name, func(t *testing.T) { - ctx, cancelFunc := context.WithTimeout(context.Background(), time.Second*5) - defer cancelFunc() - c := NewEvictCache(strategy) - - tc.before(t) - result, err := c.IncrBy(ctx, tc.key, tc.val) - assert.Equal(t, tc.wantVal, result) - assert.Equal(t, tc.wantErr, err) - tc.after(t) - }) - } -} - -func TestEvictCache_DecrBy(t *testing.T) { - evictCounter := 0 - onEvicted := func(key string, value any) { - evictCounter++ - } - strategy := lru.NewLRU[string, any]( - lru.WithCapacity[string, any](5), - lru.WithCallback[string, any](onEvicted)) - - testCase := []struct { - name string - before func(t *testing.T) - after func(t *testing.T) - - key string - val int64 - wantVal int64 - wantErr error - }{ - { - name: "decrby value", - before: func(t *testing.T) {}, - after: func(t *testing.T) { - assert.Equal(t, true, strategy.Remove("test")) - }, - key: "test", - val: 1, - wantVal: -1, - }, - { - name: "decrby old value", - before: func(t *testing.T) { - assert.Equal(t, false, strategy.Add("test", int64(3))) - }, - after: func(t *testing.T) { - assert.Equal(t, true, strategy.Remove("test")) - }, - key: "test", - val: 2, - wantVal: 1, - }, - { - name: "decrby value type error", - before: func(t *testing.T) { - assert.Equal(t, false, strategy.Add("test", 3.156)) - }, - after: func(t *testing.T) { - assert.Equal(t, true, strategy.Remove("test")) - }, - key: "test", - val: 1, - wantErr: errors.New("当前key不是int64类型"), - }, - } - - for _, tc := range testCase { - t.Run(tc.name, func(t *testing.T) { - ctx, cancelFunc := context.WithTimeout(context.Background(), time.Second*5) - defer cancelFunc() - c := NewEvictCache(strategy) - - tc.before(t) - val, err := c.DecrBy(ctx, tc.key, tc.val) - assert.Equal(t, tc.wantVal, val) - assert.Equal(t, tc.wantErr, err) - tc.after(t) - }) - } -} - -func TestEvictCache_IncrByFloat(t *testing.T) { - evictCounter := 0 - onEvicted := func(key string, value any) { - evictCounter++ - } - strategy := lru.NewLRU[string, any]( - lru.WithCapacity[string, any](5), - lru.WithCallback[string, any](onEvicted)) - - testCase := []struct { - name string - before func(t *testing.T) - after func(t *testing.T) - - key string - val float64 - wantVal float64 - wantErr error - }{ - { - name: "incrbyfloat value", - before: func(t *testing.T) {}, - after: func(t *testing.T) { - assert.Equal(t, true, strategy.Remove("test")) - }, - key: "test", - val: 2.0, - wantVal: 2.0, - }, - { - name: "incrbyfloat decr value", - before: func(t *testing.T) { - assert.Equal(t, false, strategy.Add("test", 3.1)) - }, - after: func(t *testing.T) { - assert.Equal(t, true, strategy.Remove("test")) - }, - key: "test", - val: -2.0, - wantVal: 1.1, - }, - { - name: "incrbyfloat value type error", - before: func(t *testing.T) { - assert.Equal(t, false, strategy.Add("test", "hello")) - }, - after: func(t *testing.T) { - assert.Equal(t, true, strategy.Remove("test")) - }, - key: "test", - val: 10, - wantErr: errors.New("当前key不是float64类型"), - }, - } - - for _, tc := range testCase { - t.Run(tc.name, func(t *testing.T) { - ctx, cancelFunc := context.WithTimeout(context.Background(), time.Second*5) - defer cancelFunc() - c := NewEvictCache(strategy) - - tc.before(t) - val, err := c.IncrByFloat(ctx, tc.key, tc.val) - assert.Equal(t, tc.wantVal, val) - assert.Equal(t, tc.wantErr, err) - tc.after(t) - }) - } -} - -func newCache() (ecache.Cache, error) { - strategy := newLRUStrategy(10) - return NewEvictCache(strategy), nil -} - -func newLRUStrategy(size int) *lru.LRU[string, any] { - evictCounter := 0 - onEvicted := func(key string, value any) { - evictCounter++ - } - return lru.NewLRU[string, any]( - lru.WithCapacity[string, any](size), - lru.WithCallback[string, any](onEvicted)) -} diff --git a/cacheevict/types.go b/cacheevict/types.go deleted file mode 100644 index 37b02e5..0000000 --- a/cacheevict/types.go +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright 2023 ecodeclub -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package cacheevict - -import ( - "time" - - "github.com/ecodeclub/ecache/cacheevict/lru" -) - -var _ EvictStrategy[string, any] = (*lru.LRU[string, any])(nil) - -type EvictStrategy[K comparable, V any] interface { - AddTTL(key K, value V, expiration time.Duration) bool - Add(key K, value V) bool - Get(key K) (value V, ok bool) - Contains(key K) (ok bool) - Peek(key K) (value V, ok bool) - Remove(key K) bool - RemoveOldest() (K, V, bool) - GetOldest() (K, V, bool) - Keys() []K - Values() []V - Len() int - Purge() - Resize(int) int -} diff --git a/memory/lru/cache.go b/memory/lru/cache.go index 7ae9e08..71b6cbe 100644 --- a/memory/lru/cache.go +++ b/memory/lru/cache.go @@ -27,7 +27,6 @@ import ( "github.com/ecodeclub/ecache" "github.com/ecodeclub/ecache/internal/errs" - "github.com/hashicorp/golang-lru/v2/simplelru" ) var ( @@ -35,36 +34,34 @@ var ( ) type Cache struct { - lock sync.RWMutex - client simplelru.LRUCache[string, any] + lock sync.RWMutex + lru *LRU[string, any] } -func NewCache(client simplelru.LRUCache[string, any]) *Cache { +func NewCache(lru *LRU[string, any]) *Cache { return &Cache{ - lock: sync.RWMutex{}, - client: client, + lock: sync.RWMutex{}, + lru: lru, } } -// 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.lru.AddTTL(key, val, expiration) return nil } -// SetNX expiration 无效 由lru 统一控制过期时间 +// SetNX 由 strategy 统一控制过期时间 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.lru.Contains(key) { return false, nil } - c.client.Add(key, val) + c.lru.AddTTL(key, val, expiration) return true, nil } @@ -73,7 +70,7 @@ func (c *Cache) Get(ctx context.Context, key string) (val ecache.Value) { c.lock.RLock() defer c.lock.RUnlock() var ok bool - val.Val, ok = c.client.Get(key) + val.Val, ok = c.lru.Get(key) if !ok { val.Err = errs.ErrKeyNotExist } @@ -86,12 +83,12 @@ func (c *Cache) GetSet(ctx context.Context, key string, val string) (result ecac defer c.lock.Unlock() var ok bool - result.Val, ok = c.client.Get(key) + result.Val, ok = c.lru.Get(key) if !ok { result.Err = errs.ErrKeyNotExist } - c.client.Add(key, val) + c.lru.Add(key, val) return } @@ -105,11 +102,11 @@ func (c *Cache) Delete(ctx context.Context, key ...string) (int64, error) { if ctx.Err() != nil { return n, ctx.Err() } - _, ok := c.client.Get(k) + _, ok := c.lru.Get(k) if !ok { continue } - if c.client.Remove(k) { + if c.lru.Remove(k) { n++ } else { return n, fmt.Errorf("%w: key = %s", errs.ErrDeleteKeyFailed, k) @@ -137,12 +134,12 @@ func (c *Cache) LPush(ctx context.Context, key string, val ...any) (int64, error ok bool result = ecache.Value{} ) - result.Val, ok = c.client.Get(key) + result.Val, ok = c.lru.Get(key) if !ok { l := &list.ConcurrentList[ecache.Value]{ List: list.NewLinkedListOf[ecache.Value](c.anySliceToValueSlice(val...)), } - c.client.Add(key, l) + c.lru.Add(key, l) return int64(l.Len()), nil } @@ -156,7 +153,7 @@ func (c *Cache) LPush(ctx context.Context, key string, val ...any) (int64, error return 0, err } - c.client.Add(key, data) + c.lru.Add(key, data) return int64(data.Len()), nil } @@ -167,7 +164,7 @@ func (c *Cache) LPop(ctx context.Context, key string) (val ecache.Value) { var ( ok bool ) - val.Val, ok = c.client.Get(key) + val.Val, ok = c.lru.Get(key) if !ok { val.Err = errs.ErrKeyNotExist return @@ -197,7 +194,7 @@ func (c *Cache) SAdd(ctx context.Context, key string, members ...any) (int64, er ok bool result = ecache.Value{} ) - result.Val, ok = c.client.Get(key) + result.Val, ok = c.lru.Get(key) if !ok { result.Val = set.NewMapSet[any](8) } @@ -210,7 +207,7 @@ func (c *Cache) SAdd(ctx context.Context, key string, members ...any) (int64, er for _, value := range members { s.Add(value) } - c.client.Add(key, s) + c.lru.Add(key, s) return int64(len(s.Keys())), nil } @@ -219,7 +216,7 @@ func (c *Cache) SRem(ctx context.Context, key string, members ...any) (int64, er c.lock.Lock() defer c.lock.Unlock() - result, ok := c.client.Get(key) + result, ok := c.lru.Get(key) if !ok { return 0, errs.ErrKeyNotExist } @@ -247,9 +244,9 @@ func (c *Cache) IncrBy(ctx context.Context, key string, value int64) (int64, err ok bool result = ecache.Value{} ) - result.Val, ok = c.client.Get(key) + result.Val, ok = c.lru.Get(key) if !ok { - c.client.Add(key, value) + c.lru.Add(key, value) return value, nil } @@ -259,7 +256,7 @@ func (c *Cache) IncrBy(ctx context.Context, key string, value int64) (int64, err } newVal := incr + value - c.client.Add(key, newVal) + c.lru.Add(key, newVal) return newVal, nil } @@ -272,9 +269,9 @@ func (c *Cache) DecrBy(ctx context.Context, key string, value int64) (int64, err ok bool result = ecache.Value{} ) - result.Val, ok = c.client.Get(key) + result.Val, ok = c.lru.Get(key) if !ok { - c.client.Add(key, -value) + c.lru.Add(key, -value) return -value, nil } @@ -284,7 +281,7 @@ func (c *Cache) DecrBy(ctx context.Context, key string, value int64) (int64, err } newVal := decr - value - c.client.Add(key, newVal) + c.lru.Add(key, newVal) return newVal, nil } @@ -297,9 +294,9 @@ func (c *Cache) IncrByFloat(ctx context.Context, key string, value float64) (flo ok bool result = ecache.Value{} ) - result.Val, ok = c.client.Get(key) + result.Val, ok = c.lru.Get(key) if !ok { - c.client.Add(key, value) + c.lru.Add(key, value) return value, nil } @@ -309,7 +306,7 @@ func (c *Cache) IncrByFloat(ctx context.Context, key string, value float64) (flo } newVal := val + value - c.client.Add(key, newVal) + c.lru.Add(key, newVal) return newVal, nil } diff --git a/memory/lru/cache_test.go b/memory/lru/cache_test.go index 3fa4ea9..2e01ac1 100644 --- a/memory/lru/cache_test.go +++ b/memory/lru/cache_test.go @@ -25,7 +25,6 @@ import ( "github.com/ecodeclub/ecache" "github.com/ecodeclub/ecache/internal/errs" "github.com/ecodeclub/ekit/list" - "github.com/hashicorp/golang-lru/v2/simplelru" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -35,8 +34,9 @@ func TestCache_Set(t *testing.T) { onEvicted := func(key string, value any) { evictCounter++ } - lru, err := simplelru.NewLRU[string, any](5, onEvicted) - assert.NoError(t, err) + strategy := NewLRU[string, any]( + WithCapacity[string, any](5), + WithCallback[string, any](onEvicted)) testCase := []struct { name string @@ -51,10 +51,10 @@ func TestCache_Set(t *testing.T) { { name: "set value", after: func(t *testing.T) { - result, ok := lru.Get("test") + result, ok := strategy.Get("test") assert.Equal(t, true, ok) assert.Equal(t, "hello ecache", result.(string)) - assert.Equal(t, true, lru.Remove("test")) + assert.Equal(t, true, strategy.Remove("test")) }, key: "test", val: "hello ecache", @@ -66,7 +66,7 @@ func TestCache_Set(t *testing.T) { t.Run(tc.name, func(t *testing.T) { ctx, cancelFunc := context.WithTimeout(context.Background(), time.Second*5) defer cancelFunc() - c := NewCache(lru) + c := NewCache(strategy) err := c.Set(ctx, tc.key, tc.val, tc.expiration) require.NoError(t, err) @@ -80,8 +80,9 @@ func TestCache_Get(t *testing.T) { onEvicted := func(key string, value any) { evictCounter++ } - lru, err := simplelru.NewLRU[string, any](5, onEvicted) - assert.NoError(t, err) + strategy := NewLRU[string, any]( + WithCapacity[string, any](5), + WithCallback[string, any](onEvicted)) testCase := []struct { name string @@ -96,10 +97,24 @@ func TestCache_Get(t *testing.T) { { name: "get value", before: func(t *testing.T) { - assert.Equal(t, false, lru.Add("test", "hello ecache")) + assert.Equal(t, false, strategy.Add("test", "hello ecache")) }, after: func(t *testing.T) { - assert.Equal(t, true, lru.Remove("test")) + assert.Equal(t, true, strategy.Remove("test")) + }, + key: "test", + wantVal: "hello ecache", + }, + { + name: "get expired value", + before: func(t *testing.T) { + assert.Equal(t, false, + strategy.AddTTL("test", "hello ecache", time.Second)) + }, + after: func(t *testing.T) { + time.Sleep(time.Second) + _, ok := strategy.Get("test") + assert.Equal(t, false, ok) }, key: "test", wantVal: "hello ecache", @@ -117,7 +132,7 @@ func TestCache_Get(t *testing.T) { t.Run(tc.name, func(t *testing.T) { ctx, cancelFunc := context.WithTimeout(context.Background(), time.Second*5) defer cancelFunc() - c := NewCache(lru) + c := NewCache(strategy) tc.before(t) result := c.Get(ctx, tc.key) @@ -134,8 +149,9 @@ func TestCache_SetNX(t *testing.T) { onEvicted := func(key string, value any) { evictCounter++ } - lru, err := simplelru.NewLRU[string, any](5, onEvicted) - assert.NoError(t, err) + strategy := NewLRU[string, any]( + WithCapacity[string, any](1), + WithCallback[string, any](onEvicted)) testCase := []struct { name string @@ -148,11 +164,9 @@ func TestCache_SetNX(t *testing.T) { wantVal bool }{ { - name: "setnx value", - before: func(t *testing.T) {}, - after: func(t *testing.T) { - assert.Equal(t, true, lru.Remove("test")) - }, + name: "setnx value", + before: func(t *testing.T) {}, + after: func(t *testing.T) {}, key: "test", val: "hello ecache", expire: time.Minute, @@ -161,10 +175,24 @@ func TestCache_SetNX(t *testing.T) { { name: "setnx value exist", before: func(t *testing.T) { - assert.Equal(t, false, lru.Add("test", "hello ecache")) + assert.Equal(t, false, strategy.Add("test", "hello ecache")) }, after: func(t *testing.T) { - assert.Equal(t, true, lru.Remove("test")) + assert.Equal(t, true, strategy.Remove("test")) + }, + key: "test", + val: "hello world", + expire: time.Minute, + wantVal: false, + }, + { + name: "setnx expired value", + before: func(t *testing.T) { + assert.Equal(t, false, strategy.AddTTL("test", "hello ecache", time.Second)) + }, + after: func(t *testing.T) { + time.Sleep(time.Second) + assert.Equal(t, false, strategy.Remove("test")) }, key: "test", val: "hello world", @@ -177,7 +205,7 @@ func TestCache_SetNX(t *testing.T) { t.Run(tc.name, func(t *testing.T) { ctx, cancelFunc := context.WithTimeout(context.Background(), time.Second*5) defer cancelFunc() - c := NewCache(lru) + c := NewCache(strategy) tc.before(t) result, err := c.SetNX(ctx, tc.key, tc.val, tc.expire) @@ -193,8 +221,9 @@ func TestCache_GetSet(t *testing.T) { onEvicted := func(key string, value any) { evictCounter++ } - lru, err := simplelru.NewLRU[string, any](5, onEvicted) - assert.NoError(t, err) + strategy := NewLRU[string, any]( + WithCapacity[string, any](5), + WithCallback[string, any](onEvicted)) testCase := []struct { name string @@ -209,13 +238,13 @@ func TestCache_GetSet(t *testing.T) { { name: "getset value", before: func(t *testing.T) { - assert.Equal(t, false, lru.Add("test", "hello ecache")) + assert.Equal(t, false, strategy.Add("test", "hello ecache")) }, after: func(t *testing.T) { - result, ok := lru.Get("test") + result, ok := strategy.Get("test") assert.Equal(t, true, ok) assert.Equal(t, "hello world", result) - assert.Equal(t, true, lru.Remove("test")) + assert.Equal(t, true, strategy.Remove("test")) }, key: "test", val: "hello world", @@ -225,10 +254,10 @@ func TestCache_GetSet(t *testing.T) { name: "getset value not key error", before: func(t *testing.T) {}, after: func(t *testing.T) { - result, ok := lru.Get("test") + result, ok := strategy.Get("test") assert.Equal(t, true, ok) assert.Equal(t, "hello world", result) - assert.Equal(t, true, lru.Remove("test")) + assert.Equal(t, true, strategy.Remove("test")) }, key: "test", val: "hello world", @@ -240,7 +269,7 @@ func TestCache_GetSet(t *testing.T) { t.Run(tc.name, func(t *testing.T) { ctx, cancelFunc := context.WithTimeout(context.Background(), time.Second*5) defer cancelFunc() - c := NewCache(lru) + c := NewCache(strategy) tc.before(t) result := c.GetSet(ctx, tc.key, tc.val) @@ -267,7 +296,7 @@ func TestCache_Delete(t *testing.T) { wantErr error }{ { - name: "delete single existed key", + name: "delete expired key", before: func(ctx context.Context, t *testing.T, cache ecache.Cache) { require.NoError(t, cache.Set(ctx, "name", "Alex", 0)) }, @@ -275,6 +304,17 @@ func TestCache_Delete(t *testing.T) { return context.Background() }, key: []string{"name"}, + wantN: 0, + }, + { + name: "delete single existed key", + before: func(ctx context.Context, t *testing.T, cache ecache.Cache) { + require.NoError(t, cache.Set(ctx, "name", "Alex", 10*time.Second)) + }, + ctxFunc: func() context.Context { + return context.Background() + }, + key: []string{"name"}, wantN: 1, }, { @@ -286,7 +326,7 @@ func TestCache_Delete(t *testing.T) { key: []string{"notExistedKey"}, }, { - name: "delete multiple existed keys", + name: "delete multiple expired keys", before: func(ctx context.Context, t *testing.T, cache ecache.Cache) { require.NoError(t, cache.Set(ctx, "name", "Alex", 0)) require.NoError(t, cache.Set(ctx, "age", 18, 0)) @@ -295,6 +335,18 @@ func TestCache_Delete(t *testing.T) { return context.Background() }, key: []string{"name", "age"}, + wantN: 0, + }, + { + name: "delete multiple existed keys", + before: func(ctx context.Context, t *testing.T, cache ecache.Cache) { + require.NoError(t, cache.Set(ctx, "name", "Alex", 10*time.Second)) + require.NoError(t, cache.Set(ctx, "age", 18, 10*time.Second)) + }, + ctxFunc: func() context.Context { + return context.Background() + }, + key: []string{"name", "age"}, wantN: 2, }, { @@ -306,7 +358,7 @@ func TestCache_Delete(t *testing.T) { key: []string{"name", "age"}, }, { - name: "delete multiple keys, some do not existed keys", + name: "delete multiple keys, some do not expired keys", before: func(ctx context.Context, t *testing.T, cache ecache.Cache) { require.NoError(t, cache.Set(ctx, "name", "Alex", 0)) require.NoError(t, cache.Set(ctx, "age", 18, 0)) @@ -316,6 +368,19 @@ func TestCache_Delete(t *testing.T) { return context.Background() }, key: []string{"name", "age", "gender", "addr"}, + wantN: 0, + }, + { + name: "delete multiple keys, some do not existed keys", + before: func(ctx context.Context, t *testing.T, cache ecache.Cache) { + require.NoError(t, cache.Set(ctx, "name", "Alex", 10*time.Second)) + require.NoError(t, cache.Set(ctx, "age", 18, 10*time.Second)) + require.NoError(t, cache.Set(ctx, "gender", "male", 10*time.Second)) + }, + ctxFunc: func() context.Context { + return context.Background() + }, + key: []string{"name", "age", "gender", "addr"}, wantN: 3, }, { @@ -352,8 +417,9 @@ func TestCache_LPush(t *testing.T) { onEvicted := func(key string, value any) { evictCounter++ } - lru, err := simplelru.NewLRU[string, any](5, onEvicted) - assert.NoError(t, err) + strategy := NewLRU[string, any]( + WithCapacity[string, any](5), + WithCallback[string, any](onEvicted)) testCase := []struct { name string @@ -369,7 +435,7 @@ func TestCache_LPush(t *testing.T) { name: "lpush value", before: func(t *testing.T) {}, after: func(t *testing.T) { - assert.Equal(t, true, lru.Remove("test")) + assert.Equal(t, true, strategy.Remove("test")) }, key: "test", val: []any{"hello ecache"}, @@ -379,7 +445,7 @@ func TestCache_LPush(t *testing.T) { name: "lpush multiple value", before: func(t *testing.T) {}, after: func(t *testing.T) { - assert.Equal(t, true, lru.Remove("test")) + assert.Equal(t, true, strategy.Remove("test")) }, key: "test", val: []any{"hello ecache", "hello world"}, @@ -393,10 +459,10 @@ func TestCache_LPush(t *testing.T) { l := &list.ConcurrentList[ecache.Value]{ List: list.NewLinkedListOf[ecache.Value]([]ecache.Value{val}), } - assert.Equal(t, false, lru.Add("test", l)) + assert.Equal(t, false, strategy.Add("test", l)) }, after: func(t *testing.T) { - assert.Equal(t, true, lru.Remove("test")) + assert.Equal(t, true, strategy.Remove("test")) }, key: "test", val: []any{"hello world"}, @@ -405,10 +471,10 @@ func TestCache_LPush(t *testing.T) { { name: "lpush value not type", before: func(t *testing.T) { - assert.Equal(t, false, lru.Add("test", "string")) + assert.Equal(t, false, strategy.Add("test", "string")) }, after: func(t *testing.T) { - assert.Equal(t, true, lru.Remove("test")) + assert.Equal(t, true, strategy.Remove("test")) }, key: "test", val: []any{"hello ecache"}, @@ -420,7 +486,7 @@ func TestCache_LPush(t *testing.T) { t.Run(tc.name, func(t *testing.T) { ctx, cancelFunc := context.WithTimeout(context.Background(), time.Second*5) defer cancelFunc() - c := NewCache(lru) + c := NewCache(strategy) tc.before(t) length, err := c.LPush(ctx, tc.key, tc.val...) @@ -436,8 +502,9 @@ func TestCache_LPop(t *testing.T) { onEvicted := func(key string, value any) { evictCounter++ } - lru, err := simplelru.NewLRU[string, any](5, onEvicted) - assert.NoError(t, err) + strategy := NewLRU[string, any]( + WithCapacity[string, any](5), + WithCallback[string, any](onEvicted)) testCase := []struct { name string @@ -456,10 +523,10 @@ func TestCache_LPop(t *testing.T) { l := &list.ConcurrentList[ecache.Value]{ List: list.NewLinkedListOf[ecache.Value]([]ecache.Value{val}), } - assert.Equal(t, false, lru.Add("test", l)) + assert.Equal(t, false, strategy.Add("test", l)) }, after: func(t *testing.T) { - assert.Equal(t, true, lru.Remove("test")) + assert.Equal(t, true, strategy.Remove("test")) }, key: "test", wantVal: "hello ecache", @@ -474,10 +541,10 @@ func TestCache_LPop(t *testing.T) { l := &list.ConcurrentList[ecache.Value]{ List: list.NewLinkedListOf[ecache.Value]([]ecache.Value{val, val2}), } - assert.Equal(t, false, lru.Add("test", l)) + assert.Equal(t, false, strategy.Add("test", l)) }, after: func(t *testing.T) { - val, ok := lru.Get("test") + val, ok := strategy.Get("test") assert.Equal(t, true, ok) result, ok := val.(list.List[ecache.Value]) assert.Equal(t, true, ok) @@ -487,7 +554,7 @@ func TestCache_LPop(t *testing.T) { assert.Equal(t, "hello world", value.Val) assert.NoError(t, value.Err) - assert.Equal(t, true, lru.Remove("test")) + assert.Equal(t, true, strategy.Remove("test")) }, key: "test", wantVal: "hello ecache", @@ -495,10 +562,10 @@ func TestCache_LPop(t *testing.T) { { name: "lpop value type error", before: func(t *testing.T) { - assert.Equal(t, false, lru.Add("test", "hello world")) + assert.Equal(t, false, strategy.Add("test", "hello world")) }, after: func(t *testing.T) { - assert.Equal(t, true, lru.Remove("test")) + assert.Equal(t, true, strategy.Remove("test")) }, key: "test", wantErr: errors.New("当前key不是list类型"), @@ -516,7 +583,7 @@ func TestCache_LPop(t *testing.T) { t.Run(tc.name, func(t *testing.T) { ctx, cancelFunc := context.WithTimeout(context.Background(), time.Second*5) defer cancelFunc() - c := NewCache(lru) + c := NewCache(strategy) tc.before(t) val := c.LPop(ctx, tc.key) @@ -533,8 +600,9 @@ func TestCache_SAdd(t *testing.T) { onEvicted := func(key string, value any) { evictCounter++ } - lru, err := simplelru.NewLRU[string, any](5, onEvicted) - assert.NoError(t, err) + strategy := NewLRU[string, any]( + WithCapacity[string, any](5), + WithCallback[string, any](onEvicted)) testCase := []struct { name string @@ -550,7 +618,7 @@ func TestCache_SAdd(t *testing.T) { name: "sadd value", before: func(t *testing.T) {}, after: func(t *testing.T) { - assert.Equal(t, true, lru.Remove("test")) + assert.Equal(t, true, strategy.Remove("test")) }, key: "test", val: []any{"hello ecache", "hello world"}, @@ -562,10 +630,10 @@ func TestCache_SAdd(t *testing.T) { s := set.NewMapSet[any](8) s.Add("hello world") - assert.Equal(t, false, lru.Add("test", s)) + assert.Equal(t, false, strategy.Add("test", s)) }, after: func(t *testing.T) { - assert.Equal(t, true, lru.Remove("test")) + assert.Equal(t, true, strategy.Remove("test")) }, key: "test", val: []any{"hello ecache"}, @@ -574,10 +642,10 @@ func TestCache_SAdd(t *testing.T) { { name: "sadd value type err", before: func(t *testing.T) { - assert.Equal(t, false, lru.Add("test", "string")) + assert.Equal(t, false, strategy.Add("test", "string")) }, after: func(t *testing.T) { - assert.Equal(t, true, lru.Remove("test")) + assert.Equal(t, true, strategy.Remove("test")) }, key: "test", val: []any{"hello"}, @@ -589,7 +657,7 @@ func TestCache_SAdd(t *testing.T) { t.Run(tc.name, func(t *testing.T) { ctx, cancelFunc := context.WithTimeout(context.Background(), time.Second*5) defer cancelFunc() - c := NewCache(lru) + c := NewCache(strategy) tc.before(t) val, err := c.SAdd(ctx, tc.key, tc.val...) @@ -605,8 +673,9 @@ func TestCache_SRem(t *testing.T) { onEvicted := func(key string, value any) { evictCounter++ } - lru, err := simplelru.NewLRU[string, any](5, onEvicted) - assert.NoError(t, err) + strategy := NewLRU[string, any]( + WithCapacity[string, any](5), + WithCallback[string, any](onEvicted)) testCase := []struct { name string @@ -627,10 +696,10 @@ func TestCache_SRem(t *testing.T) { s.Add("hello world") s.Add("hello ecache") - assert.Equal(t, false, lru.Add("test", s)) + assert.Equal(t, false, strategy.Add("test", s)) }, after: func(t *testing.T) { - assert.Equal(t, true, lru.Remove("test")) + assert.Equal(t, true, strategy.Remove("test")) }, key: "test", val: []any{"hello world"}, @@ -642,10 +711,10 @@ func TestCache_SRem(t *testing.T) { s := set.NewMapSet[any](8) s.Add("hello world") - assert.Equal(t, false, lru.Add("test", s)) + assert.Equal(t, false, strategy.Add("test", s)) }, after: func(t *testing.T) { - assert.Equal(t, true, lru.Remove("test")) + assert.Equal(t, true, strategy.Remove("test")) }, key: "test", val: []any{"hello ecache"}, @@ -662,10 +731,10 @@ func TestCache_SRem(t *testing.T) { { name: "srem value type error", before: func(t *testing.T) { - assert.Equal(t, false, lru.Add("test", int64(1))) + assert.Equal(t, false, strategy.Add("test", int64(1))) }, after: func(t *testing.T) { - assert.Equal(t, true, lru.Remove("test")) + assert.Equal(t, true, strategy.Remove("test")) }, key: "test", val: []any{"hello world"}, @@ -678,7 +747,7 @@ func TestCache_SRem(t *testing.T) { defer tc.after(t) ctx, cancelFunc := context.WithTimeout(context.Background(), time.Second*5) defer cancelFunc() - c := NewCache(lru) + c := NewCache(strategy) tc.before(t) val, err := c.SRem(ctx, tc.key, tc.val...) @@ -693,8 +762,9 @@ func TestCache_IncrBy(t *testing.T) { onEvicted := func(key string, value any) { evictCounter++ } - lru, err := simplelru.NewLRU[string, any](5, onEvicted) - assert.NoError(t, err) + strategy := NewLRU[string, any]( + WithCapacity[string, any](5), + WithCallback[string, any](onEvicted)) testCase := []struct { name string @@ -710,7 +780,7 @@ func TestCache_IncrBy(t *testing.T) { name: "incrby value", before: func(t *testing.T) {}, after: func(t *testing.T) { - assert.Equal(t, true, lru.Remove("test")) + assert.Equal(t, true, strategy.Remove("test")) }, key: "test", val: 1, @@ -719,10 +789,10 @@ func TestCache_IncrBy(t *testing.T) { { name: "incrby value add", before: func(t *testing.T) { - assert.Equal(t, false, lru.Add("test", int64(1))) + assert.Equal(t, false, strategy.Add("test", int64(1))) }, after: func(t *testing.T) { - assert.Equal(t, true, lru.Remove("test")) + assert.Equal(t, true, strategy.Remove("test")) }, key: "test", val: 1, @@ -731,10 +801,10 @@ func TestCache_IncrBy(t *testing.T) { { name: "incrby value type error", before: func(t *testing.T) { - assert.Equal(t, false, lru.Add("test", 12.62)) + assert.Equal(t, false, strategy.Add("test", 12.62)) }, after: func(t *testing.T) { - assert.Equal(t, true, lru.Remove("test")) + assert.Equal(t, true, strategy.Remove("test")) }, key: "test", val: 1, @@ -746,7 +816,7 @@ func TestCache_IncrBy(t *testing.T) { t.Run(tc.name, func(t *testing.T) { ctx, cancelFunc := context.WithTimeout(context.Background(), time.Second*5) defer cancelFunc() - c := NewCache(lru) + c := NewCache(strategy) tc.before(t) result, err := c.IncrBy(ctx, tc.key, tc.val) @@ -762,8 +832,9 @@ func TestCache_DecrBy(t *testing.T) { onEvicted := func(key string, value any) { evictCounter++ } - lru, err := simplelru.NewLRU[string, any](5, onEvicted) - assert.NoError(t, err) + strategy := NewLRU[string, any]( + WithCapacity[string, any](5), + WithCallback[string, any](onEvicted)) testCase := []struct { name string @@ -779,7 +850,7 @@ func TestCache_DecrBy(t *testing.T) { name: "decrby value", before: func(t *testing.T) {}, after: func(t *testing.T) { - assert.Equal(t, true, lru.Remove("test")) + assert.Equal(t, true, strategy.Remove("test")) }, key: "test", val: 1, @@ -788,10 +859,10 @@ func TestCache_DecrBy(t *testing.T) { { name: "decrby old value", before: func(t *testing.T) { - assert.Equal(t, false, lru.Add("test", int64(3))) + assert.Equal(t, false, strategy.Add("test", int64(3))) }, after: func(t *testing.T) { - assert.Equal(t, true, lru.Remove("test")) + assert.Equal(t, true, strategy.Remove("test")) }, key: "test", val: 2, @@ -800,10 +871,10 @@ func TestCache_DecrBy(t *testing.T) { { name: "decrby value type error", before: func(t *testing.T) { - assert.Equal(t, false, lru.Add("test", 3.156)) + assert.Equal(t, false, strategy.Add("test", 3.156)) }, after: func(t *testing.T) { - assert.Equal(t, true, lru.Remove("test")) + assert.Equal(t, true, strategy.Remove("test")) }, key: "test", val: 1, @@ -815,7 +886,7 @@ func TestCache_DecrBy(t *testing.T) { t.Run(tc.name, func(t *testing.T) { ctx, cancelFunc := context.WithTimeout(context.Background(), time.Second*5) defer cancelFunc() - c := NewCache(lru) + c := NewCache(strategy) tc.before(t) val, err := c.DecrBy(ctx, tc.key, tc.val) @@ -831,8 +902,9 @@ func TestCache_IncrByFloat(t *testing.T) { onEvicted := func(key string, value any) { evictCounter++ } - lru, err := simplelru.NewLRU[string, any](5, onEvicted) - assert.NoError(t, err) + strategy := NewLRU[string, any]( + WithCapacity[string, any](5), + WithCallback[string, any](onEvicted)) testCase := []struct { name string @@ -848,7 +920,7 @@ func TestCache_IncrByFloat(t *testing.T) { name: "incrbyfloat value", before: func(t *testing.T) {}, after: func(t *testing.T) { - assert.Equal(t, true, lru.Remove("test")) + assert.Equal(t, true, strategy.Remove("test")) }, key: "test", val: 2.0, @@ -857,10 +929,10 @@ func TestCache_IncrByFloat(t *testing.T) { { name: "incrbyfloat decr value", before: func(t *testing.T) { - assert.Equal(t, false, lru.Add("test", 3.1)) + assert.Equal(t, false, strategy.Add("test", 3.1)) }, after: func(t *testing.T) { - assert.Equal(t, true, lru.Remove("test")) + assert.Equal(t, true, strategy.Remove("test")) }, key: "test", val: -2.0, @@ -869,10 +941,10 @@ func TestCache_IncrByFloat(t *testing.T) { { name: "incrbyfloat value type error", before: func(t *testing.T) { - assert.Equal(t, false, lru.Add("test", "hello")) + assert.Equal(t, false, strategy.Add("test", "hello")) }, after: func(t *testing.T) { - assert.Equal(t, true, lru.Remove("test")) + assert.Equal(t, true, strategy.Remove("test")) }, key: "test", val: 10, @@ -884,7 +956,7 @@ func TestCache_IncrByFloat(t *testing.T) { t.Run(tc.name, func(t *testing.T) { ctx, cancelFunc := context.WithTimeout(context.Background(), time.Second*5) defer cancelFunc() - c := NewCache(lru) + c := NewCache(strategy) tc.before(t) val, err := c.IncrByFloat(ctx, tc.key, tc.val) @@ -896,17 +968,16 @@ func TestCache_IncrByFloat(t *testing.T) { } func newCache() (ecache.Cache, error) { - client, err := newSimpleLRUClient(10) - if err != nil { - return nil, err - } - return NewCache(client), nil + strategy := newLRUStrategy(10) + return NewCache(strategy), nil } -func newSimpleLRUClient(size int) (simplelru.LRUCache[string, any], error) { +func newLRUStrategy(size int) *LRU[string, any] { evictCounter := 0 onEvicted := func(key string, value any) { evictCounter++ } - return simplelru.NewLRU[string, any](size, onEvicted) + return NewLRU[string, any]( + WithCapacity[string, any](size), + WithCallback[string, any](onEvicted)) } diff --git a/cacheevict/lru/list.go b/memory/lru/list.go similarity index 100% rename from cacheevict/lru/list.go rename to memory/lru/list.go diff --git a/cacheevict/lru/list_test.go b/memory/lru/list_test.go similarity index 100% rename from cacheevict/lru/list_test.go rename to memory/lru/list_test.go diff --git a/cacheevict/lru/lru.go b/memory/lru/lru.go similarity index 90% rename from cacheevict/lru/lru.go rename to memory/lru/lru.go index 72f1bcc..770d062 100644 --- a/cacheevict/lru/lru.go +++ b/memory/lru/lru.go @@ -23,20 +23,18 @@ const ( defaultCapacity = 100 ) -var defaultExpiresAt = time.Time{} - -type Entry[K comparable, V any] struct { +type entry[K comparable, V any] struct { key K value V expiresAt time.Time } -func (e Entry[K, V]) isExpired() bool { +func (e entry[K, V]) isExpired() bool { return e.expiresAt.Before(time.Now()) } -func (e Entry[K, V]) existExpiration() bool { - return !e.expiresAt.Equal(defaultExpiresAt) +func (e entry[K, V]) existExpiration() bool { + return !e.expiresAt.Equal(time.Time{}) } type EvictCallback[K comparable, V any] func(key K, value V) @@ -58,15 +56,15 @@ func WithCapacity[K comparable, V any](capacity int) Option[K, V] { type LRU[K comparable, V any] struct { lock sync.RWMutex capacity int - list *LinkedList[Entry[K, V]] - data map[K]*Element[Entry[K, V]] + list *LinkedList[entry[K, V]] + data map[K]*Element[entry[K, V]] callback EvictCallback[K, V] } func NewLRU[K comparable, V any](options ...Option[K, V]) *LRU[K, V] { res := &LRU[K, V]{ - list: NewLinkedList[Entry[K, V]](), - data: make(map[K]*Element[Entry[K, V]], 16), + list: NewLinkedList[entry[K, V]](), + data: make(map[K]*Element[entry[K, V]], 16), capacity: defaultCapacity, } for _, opt := range options { @@ -87,7 +85,7 @@ func (l *LRU[K, V]) Purge() { l.list.Init() } -func (l *LRU[K, V]) pushEntry(key K, ent Entry[K, V]) (evicted bool) { +func (l *LRU[K, V]) pushEntry(key K, ent entry[K, V]) (evicted bool) { if elem, ok := l.data[key]; ok { elem.Value = ent l.list.MoveToFront(elem) @@ -103,14 +101,13 @@ func (l *LRU[K, V]) pushEntry(key K, ent Entry[K, V]) (evicted bool) { } func (l *LRU[K, V]) addTTL(key K, value V, expiration time.Duration) (evicted bool) { - ent := Entry[K, V]{key: key, value: value, + ent := entry[K, V]{key: key, value: value, expiresAt: time.Now().Add(expiration)} return l.pushEntry(key, ent) } func (l *LRU[K, V]) add(key K, value V) (evicted bool) { - ent := Entry[K, V]{key: key, value: value, - expiresAt: defaultExpiresAt} + ent := entry[K, V]{key: key, value: value} return l.pushEntry(key, ent) } @@ -190,7 +187,7 @@ func (l *LRU[K, V]) removeOldest() { } } -func (l *LRU[K, V]) removeElement(elem *Element[Entry[K, V]]) { +func (l *LRU[K, V]) removeElement(elem *Element[entry[K, V]]) { l.list.Remove(elem) entry := elem.Value l.delete(entry.key) diff --git a/cacheevict/lru/lru_test.go b/memory/lru/lru_test.go similarity index 100% rename from cacheevict/lru/lru_test.go rename to memory/lru/lru_test.go From 7103f2fc8813189d3797221260f35daa80a2f8f4 Mon Sep 17 00:00:00 2001 From: Stone-afk <1711865140@qq.com> Date: Sun, 24 Dec 2023 17:24:04 +0800 Subject: [PATCH 04/16] feature add lru cache evict --- go.mod | 1 - go.sum | 2 -- 2 files changed, 3 deletions(-) diff --git a/go.mod b/go.mod index ad0b436..76ce092 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 6300369..df454ee 100644 --- a/go.sum +++ b/go.sum @@ -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= From c42c60ce5bd15e72880cef51a55edeaa256379f3 Mon Sep 17 00:00:00 2001 From: Stone-afk <1711865140@qq.com> Date: Sun, 24 Dec 2023 23:46:35 +0800 Subject: [PATCH 05/16] feature add lru cache evict --- memory/lru/cache.go | 222 +++++++++++++++++++++---- memory/lru/cache_test.go | 203 +++++++++-------------- memory/lru/list.go | 60 +++---- memory/lru/list_test.go | 122 +++++++------- memory/lru/lru.go | 295 --------------------------------- memory/lru/lru_test.go | 341 --------------------------------------- 6 files changed, 358 insertions(+), 885 deletions(-) delete mode 100644 memory/lru/lru.go delete mode 100644 memory/lru/lru_test.go diff --git a/memory/lru/cache.go b/memory/lru/cache.go index 71b6cbe..f785c07 100644 --- a/memory/lru/cache.go +++ b/memory/lru/cache.go @@ -33,22 +33,180 @@ var ( _ ecache.Cache = (*Cache)(nil) ) +//type entry[K comparable, V any] struct { +// key K +// value V +// expiresAt time.Time +//} +// +//func (e entry[K, V]) isExpired() bool { +// return e.expiresAt.Before(time.Now()) +//} +// +//func (e entry[K, V]) existExpiration() bool { +// return !e.expiresAt.IsZero() +//} + +const ( + defaultCapacity = 100 +) + +type entry struct { + key string + value any + expiresAt time.Time +} + +func (e entry) isExpired() bool { + return e.expiresAt.Before(time.Now()) +} + +func (e entry) existExpiration() bool { + return !e.expiresAt.IsZero() +} + +type EvictCallback func(key string, value any) + +type Option func(l *Cache) + +func WithCallback(callback func(k string, v any)) Option { + return func(l *Cache) { + l.callback = callback + } +} + +func WithCapacity(capacity int) Option { + return func(l *Cache) { + l.capacity = capacity + } +} + type Cache struct { - lock sync.RWMutex - lru *LRU[string, any] + lock sync.RWMutex + capacity int + list *linkedList[entry] + data map[string]*element[entry] + callback EvictCallback +} + +func NewCache(options ...Option) *Cache { + res := &Cache{ + list: newLinkedList[entry](), + data: make(map[string]*element[entry], 16), + capacity: defaultCapacity, + } + for _, opt := range options { + opt(res) + } + return res +} + +func (c *Cache) pushEntry(key string, ent entry) (evicted bool) { + 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 + evict := c.len() > c.capacity + if evict { + c.removeOldest() + } + return evict +} + +func (c *Cache) addTTL(key string, value any, expiration time.Duration) (evicted 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) (evicted bool) { + ent := entry{key: key, value: value} + return c.pushEntry(key, ent) } -func NewCache(lru *LRU[string, any]) *Cache { - return &Cache{ - lock: sync.RWMutex{}, - lru: lru, +func (c *Cache) get(key string) (value any, ok bool) { + if elem, exist := c.data[key]; exist { + ent := elem.Value + if ent.existExpiration() && ent.isExpired() { + c.removeElement(elem) + return + } + c.list.MoveToFront(elem) + return ent.value, true + } + return +} + +func (c *Cache) RemoveOldest() (key string, value any, ok bool) { + c.lock.Lock() + defer c.lock.Unlock() + if elem := c.list.Back(); elem != nil { + c.removeElement(elem) + return elem.Value.key, elem.Value.value, true + } + return +} + +func (c *Cache) removeOldest() { + if elem := c.list.Back(); elem != nil { + c.removeElement(elem) + } +} + +func (c *Cache) removeElement(elem *element[entry]) { + c.list.Remove(elem) + ent := elem.Value + c.delete(ent.key) + if c.callback != nil { + c.callback(ent.key, ent.value) } } +func (c *Cache) remove(key string) (present bool) { + if elem, ok := c.data[key]; ok { + c.removeElement(elem) + if elem.Value.existExpiration() && elem.Value.isExpired() { + return false + } + return true + } + return false +} + +func (c *Cache) contains(key string) (ok bool) { + elem, ok := c.data[key] + if ok { + if elem.Value.existExpiration() && elem.Value.isExpired() { + c.removeElement(elem) + return false + } + } + return ok +} + +func (c *Cache) delete(key string) { + delete(c.data, key) +} + +func (c *Cache) len() int { + var length int + for elem := c.list.Back(); elem != nil; elem = elem.Prev() { + if elem.Value.existExpiration() && elem.Value.isExpired() { + c.removeElement(elem) + continue + } + length++ + } + return length +} + func (c *Cache) Set(ctx context.Context, key string, val any, expiration time.Duration) error { c.lock.Lock() defer c.lock.Unlock() - c.lru.AddTTL(key, val, expiration) + c.addTTL(key, val, expiration) return nil } @@ -57,20 +215,20 @@ func (c *Cache) SetNX(ctx context.Context, key string, val any, expiration time. c.lock.Lock() defer c.lock.Unlock() - if c.lru.Contains(key) { + if c.contains(key) { return false, nil } - c.lru.AddTTL(key, val, expiration) + 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.lru.Get(key) + val.Val, ok = c.get(key) if !ok { val.Err = errs.ErrKeyNotExist } @@ -83,12 +241,12 @@ func (c *Cache) GetSet(ctx context.Context, key string, val string) (result ecac defer c.lock.Unlock() var ok bool - result.Val, ok = c.lru.Get(key) + result.Val, ok = c.get(key) if !ok { result.Err = errs.ErrKeyNotExist } - c.lru.Add(key, val) + c.add(key, val) return } @@ -102,11 +260,11 @@ func (c *Cache) Delete(ctx context.Context, key ...string) (int64, error) { if ctx.Err() != nil { return n, ctx.Err() } - _, ok := c.lru.Get(k) + _, ok := c.get(k) if !ok { continue } - if c.lru.Remove(k) { + if c.remove(k) { n++ } else { return n, fmt.Errorf("%w: key = %s", errs.ErrDeleteKeyFailed, k) @@ -134,12 +292,12 @@ func (c *Cache) LPush(ctx context.Context, key string, val ...any) (int64, error ok bool result = ecache.Value{} ) - result.Val, ok = c.lru.Get(key) + result.Val, ok = c.get(key) if !ok { l := &list.ConcurrentList[ecache.Value]{ List: list.NewLinkedListOf[ecache.Value](c.anySliceToValueSlice(val...)), } - c.lru.Add(key, l) + c.add(key, l) return int64(l.Len()), nil } @@ -153,7 +311,7 @@ func (c *Cache) LPush(ctx context.Context, key string, val ...any) (int64, error return 0, err } - c.lru.Add(key, data) + c.add(key, data) return int64(data.Len()), nil } @@ -164,7 +322,7 @@ func (c *Cache) LPop(ctx context.Context, key string) (val ecache.Value) { var ( ok bool ) - val.Val, ok = c.lru.Get(key) + val.Val, ok = c.get(key) if !ok { val.Err = errs.ErrKeyNotExist return @@ -194,7 +352,7 @@ func (c *Cache) SAdd(ctx context.Context, key string, members ...any) (int64, er ok bool result = ecache.Value{} ) - result.Val, ok = c.lru.Get(key) + result.Val, ok = c.get(key) if !ok { result.Val = set.NewMapSet[any](8) } @@ -207,7 +365,7 @@ func (c *Cache) SAdd(ctx context.Context, key string, members ...any) (int64, er for _, value := range members { s.Add(value) } - c.lru.Add(key, s) + c.add(key, s) return int64(len(s.Keys())), nil } @@ -216,7 +374,7 @@ func (c *Cache) SRem(ctx context.Context, key string, members ...any) (int64, er c.lock.Lock() defer c.lock.Unlock() - result, ok := c.lru.Get(key) + result, ok := c.get(key) if !ok { return 0, errs.ErrKeyNotExist } @@ -244,9 +402,9 @@ func (c *Cache) IncrBy(ctx context.Context, key string, value int64) (int64, err ok bool result = ecache.Value{} ) - result.Val, ok = c.lru.Get(key) + result.Val, ok = c.get(key) if !ok { - c.lru.Add(key, value) + c.add(key, value) return value, nil } @@ -256,7 +414,7 @@ func (c *Cache) IncrBy(ctx context.Context, key string, value int64) (int64, err } newVal := incr + value - c.lru.Add(key, newVal) + c.add(key, newVal) return newVal, nil } @@ -269,9 +427,9 @@ func (c *Cache) DecrBy(ctx context.Context, key string, value int64) (int64, err ok bool result = ecache.Value{} ) - result.Val, ok = c.lru.Get(key) + result.Val, ok = c.get(key) if !ok { - c.lru.Add(key, -value) + c.add(key, -value) return -value, nil } @@ -281,7 +439,7 @@ func (c *Cache) DecrBy(ctx context.Context, key string, value int64) (int64, err } newVal := decr - value - c.lru.Add(key, newVal) + c.add(key, newVal) return newVal, nil } @@ -294,9 +452,9 @@ func (c *Cache) IncrByFloat(ctx context.Context, key string, value float64) (flo ok bool result = ecache.Value{} ) - result.Val, ok = c.lru.Get(key) + result.Val, ok = c.get(key) if !ok { - c.lru.Add(key, value) + c.add(key, value) return value, nil } @@ -306,7 +464,7 @@ func (c *Cache) IncrByFloat(ctx context.Context, key string, value float64) (flo } newVal := val + value - c.lru.Add(key, newVal) + c.add(key, newVal) return newVal, nil } diff --git a/memory/lru/cache_test.go b/memory/lru/cache_test.go index 2e01ac1..42cdfab 100644 --- a/memory/lru/cache_test.go +++ b/memory/lru/cache_test.go @@ -34,9 +34,7 @@ func TestCache_Set(t *testing.T) { onEvicted := func(key string, value any) { evictCounter++ } - strategy := NewLRU[string, any]( - WithCapacity[string, any](5), - WithCallback[string, any](onEvicted)) + cache := NewCache(WithCapacity(5), WithCallback(onEvicted)) testCase := []struct { name string @@ -51,10 +49,10 @@ func TestCache_Set(t *testing.T) { { name: "set value", after: func(t *testing.T) { - result, ok := strategy.Get("test") + result, ok := cache.get("test") assert.Equal(t, true, ok) assert.Equal(t, "hello ecache", result.(string)) - assert.Equal(t, true, strategy.Remove("test")) + assert.Equal(t, true, cache.remove("test")) }, key: "test", val: "hello ecache", @@ -66,9 +64,8 @@ func TestCache_Set(t *testing.T) { t.Run(tc.name, func(t *testing.T) { ctx, cancelFunc := context.WithTimeout(context.Background(), time.Second*5) defer cancelFunc() - c := NewCache(strategy) - err := c.Set(ctx, tc.key, tc.val, tc.expiration) + err := cache.Set(ctx, tc.key, tc.val, tc.expiration) require.NoError(t, err) tc.after(t) }) @@ -80,9 +77,7 @@ func TestCache_Get(t *testing.T) { onEvicted := func(key string, value any) { evictCounter++ } - strategy := NewLRU[string, any]( - WithCapacity[string, any](5), - WithCallback[string, any](onEvicted)) + cache := NewCache(WithCapacity(5), WithCallback(onEvicted)) testCase := []struct { name string @@ -97,10 +92,10 @@ func TestCache_Get(t *testing.T) { { name: "get value", before: func(t *testing.T) { - assert.Equal(t, false, strategy.Add("test", "hello ecache")) + assert.Equal(t, false, cache.add("test", "hello ecache")) }, after: func(t *testing.T) { - assert.Equal(t, true, strategy.Remove("test")) + assert.Equal(t, true, cache.remove("test")) }, key: "test", wantVal: "hello ecache", @@ -109,11 +104,11 @@ func TestCache_Get(t *testing.T) { name: "get expired value", before: func(t *testing.T) { assert.Equal(t, false, - strategy.AddTTL("test", "hello ecache", time.Second)) + cache.addTTL("test", "hello ecache", time.Second)) }, after: func(t *testing.T) { time.Sleep(time.Second) - _, ok := strategy.Get("test") + _, ok := cache.get("test") assert.Equal(t, false, ok) }, key: "test", @@ -132,10 +127,9 @@ func TestCache_Get(t *testing.T) { t.Run(tc.name, func(t *testing.T) { ctx, cancelFunc := context.WithTimeout(context.Background(), time.Second*5) defer cancelFunc() - c := NewCache(strategy) tc.before(t) - result := c.Get(ctx, tc.key) + result := cache.Get(ctx, tc.key) val, err := result.String() assert.Equal(t, tc.wantVal, val) assert.Equal(t, tc.wantErr, err) @@ -149,9 +143,7 @@ func TestCache_SetNX(t *testing.T) { onEvicted := func(key string, value any) { evictCounter++ } - strategy := NewLRU[string, any]( - WithCapacity[string, any](1), - WithCallback[string, any](onEvicted)) + cache := NewCache(WithCapacity(1), WithCallback(onEvicted)) testCase := []struct { name string @@ -175,10 +167,10 @@ func TestCache_SetNX(t *testing.T) { { name: "setnx value exist", before: func(t *testing.T) { - assert.Equal(t, false, strategy.Add("test", "hello ecache")) + assert.Equal(t, false, cache.add("test", "hello ecache")) }, after: func(t *testing.T) { - assert.Equal(t, true, strategy.Remove("test")) + assert.Equal(t, true, cache.remove("test")) }, key: "test", val: "hello world", @@ -188,11 +180,11 @@ func TestCache_SetNX(t *testing.T) { { name: "setnx expired value", before: func(t *testing.T) { - assert.Equal(t, false, strategy.AddTTL("test", "hello ecache", time.Second)) + assert.Equal(t, false, cache.addTTL("test", "hello ecache", time.Second)) }, after: func(t *testing.T) { time.Sleep(time.Second) - assert.Equal(t, false, strategy.Remove("test")) + assert.Equal(t, false, cache.remove("test")) }, key: "test", val: "hello world", @@ -205,10 +197,9 @@ func TestCache_SetNX(t *testing.T) { t.Run(tc.name, func(t *testing.T) { ctx, cancelFunc := context.WithTimeout(context.Background(), time.Second*5) defer cancelFunc() - c := NewCache(strategy) tc.before(t) - result, err := c.SetNX(ctx, tc.key, tc.val, tc.expire) + result, err := cache.SetNX(ctx, tc.key, tc.val, tc.expire) assert.Equal(t, tc.wantVal, result) require.NoError(t, err) tc.after(t) @@ -221,9 +212,7 @@ func TestCache_GetSet(t *testing.T) { onEvicted := func(key string, value any) { evictCounter++ } - strategy := NewLRU[string, any]( - WithCapacity[string, any](5), - WithCallback[string, any](onEvicted)) + cache := NewCache(WithCapacity(5), WithCallback(onEvicted)) testCase := []struct { name string @@ -238,13 +227,13 @@ func TestCache_GetSet(t *testing.T) { { name: "getset value", before: func(t *testing.T) { - assert.Equal(t, false, strategy.Add("test", "hello ecache")) + assert.Equal(t, false, cache.add("test", "hello ecache")) }, after: func(t *testing.T) { - result, ok := strategy.Get("test") + result, ok := cache.get("test") assert.Equal(t, true, ok) assert.Equal(t, "hello world", result) - assert.Equal(t, true, strategy.Remove("test")) + assert.Equal(t, true, cache.remove("test")) }, key: "test", val: "hello world", @@ -254,10 +243,10 @@ func TestCache_GetSet(t *testing.T) { name: "getset value not key error", before: func(t *testing.T) {}, after: func(t *testing.T) { - result, ok := strategy.Get("test") + result, ok := cache.get("test") assert.Equal(t, true, ok) assert.Equal(t, "hello world", result) - assert.Equal(t, true, strategy.Remove("test")) + assert.Equal(t, true, cache.remove("test")) }, key: "test", val: "hello world", @@ -269,10 +258,9 @@ func TestCache_GetSet(t *testing.T) { t.Run(tc.name, func(t *testing.T) { ctx, cancelFunc := context.WithTimeout(context.Background(), time.Second*5) defer cancelFunc() - c := NewCache(strategy) tc.before(t) - result := c.GetSet(ctx, tc.key, tc.val) + result := cache.GetSet(ctx, tc.key, tc.val) val, err := result.String() assert.Equal(t, tc.wantVal, val) assert.Equal(t, tc.wantErr, err) @@ -282,8 +270,7 @@ func TestCache_GetSet(t *testing.T) { } func TestCache_Delete(t *testing.T) { - cache, err := newCache() - require.NoError(t, err) + cache := NewCache(WithCapacity(5)) testCases := []struct { name string @@ -417,9 +404,7 @@ func TestCache_LPush(t *testing.T) { onEvicted := func(key string, value any) { evictCounter++ } - strategy := NewLRU[string, any]( - WithCapacity[string, any](5), - WithCallback[string, any](onEvicted)) + cache := NewCache(WithCapacity(5), WithCallback(onEvicted)) testCase := []struct { name string @@ -435,7 +420,7 @@ func TestCache_LPush(t *testing.T) { name: "lpush value", before: func(t *testing.T) {}, after: func(t *testing.T) { - assert.Equal(t, true, strategy.Remove("test")) + assert.Equal(t, true, cache.remove("test")) }, key: "test", val: []any{"hello ecache"}, @@ -445,7 +430,7 @@ func TestCache_LPush(t *testing.T) { name: "lpush multiple value", before: func(t *testing.T) {}, after: func(t *testing.T) { - assert.Equal(t, true, strategy.Remove("test")) + assert.Equal(t, true, cache.remove("test")) }, key: "test", val: []any{"hello ecache", "hello world"}, @@ -459,10 +444,10 @@ func TestCache_LPush(t *testing.T) { l := &list.ConcurrentList[ecache.Value]{ List: list.NewLinkedListOf[ecache.Value]([]ecache.Value{val}), } - assert.Equal(t, false, strategy.Add("test", l)) + assert.Equal(t, false, cache.add("test", l)) }, after: func(t *testing.T) { - assert.Equal(t, true, strategy.Remove("test")) + assert.Equal(t, true, cache.remove("test")) }, key: "test", val: []any{"hello world"}, @@ -471,10 +456,10 @@ func TestCache_LPush(t *testing.T) { { name: "lpush value not type", before: func(t *testing.T) { - assert.Equal(t, false, strategy.Add("test", "string")) + assert.Equal(t, false, cache.add("test", "string")) }, after: func(t *testing.T) { - assert.Equal(t, true, strategy.Remove("test")) + assert.Equal(t, true, cache.remove("test")) }, key: "test", val: []any{"hello ecache"}, @@ -486,10 +471,9 @@ func TestCache_LPush(t *testing.T) { t.Run(tc.name, func(t *testing.T) { ctx, cancelFunc := context.WithTimeout(context.Background(), time.Second*5) defer cancelFunc() - c := NewCache(strategy) tc.before(t) - length, err := c.LPush(ctx, tc.key, tc.val...) + length, err := cache.LPush(ctx, tc.key, tc.val...) assert.Equal(t, tc.wantVal, length) assert.Equal(t, tc.wantErr, err) tc.after(t) @@ -502,9 +486,7 @@ func TestCache_LPop(t *testing.T) { onEvicted := func(key string, value any) { evictCounter++ } - strategy := NewLRU[string, any]( - WithCapacity[string, any](5), - WithCallback[string, any](onEvicted)) + cache := NewCache(WithCapacity(5), WithCallback(onEvicted)) testCase := []struct { name string @@ -523,10 +505,10 @@ func TestCache_LPop(t *testing.T) { l := &list.ConcurrentList[ecache.Value]{ List: list.NewLinkedListOf[ecache.Value]([]ecache.Value{val}), } - assert.Equal(t, false, strategy.Add("test", l)) + assert.Equal(t, false, cache.add("test", l)) }, after: func(t *testing.T) { - assert.Equal(t, true, strategy.Remove("test")) + assert.Equal(t, true, cache.remove("test")) }, key: "test", wantVal: "hello ecache", @@ -541,10 +523,10 @@ func TestCache_LPop(t *testing.T) { l := &list.ConcurrentList[ecache.Value]{ List: list.NewLinkedListOf[ecache.Value]([]ecache.Value{val, val2}), } - assert.Equal(t, false, strategy.Add("test", l)) + assert.Equal(t, false, cache.add("test", l)) }, after: func(t *testing.T) { - val, ok := strategy.Get("test") + val, ok := cache.get("test") assert.Equal(t, true, ok) result, ok := val.(list.List[ecache.Value]) assert.Equal(t, true, ok) @@ -554,7 +536,7 @@ func TestCache_LPop(t *testing.T) { assert.Equal(t, "hello world", value.Val) assert.NoError(t, value.Err) - assert.Equal(t, true, strategy.Remove("test")) + assert.Equal(t, true, cache.remove("test")) }, key: "test", wantVal: "hello ecache", @@ -562,10 +544,10 @@ func TestCache_LPop(t *testing.T) { { name: "lpop value type error", before: func(t *testing.T) { - assert.Equal(t, false, strategy.Add("test", "hello world")) + assert.Equal(t, false, cache.add("test", "hello world")) }, after: func(t *testing.T) { - assert.Equal(t, true, strategy.Remove("test")) + assert.Equal(t, true, cache.remove("test")) }, key: "test", wantErr: errors.New("当前key不是list类型"), @@ -583,10 +565,9 @@ func TestCache_LPop(t *testing.T) { t.Run(tc.name, func(t *testing.T) { ctx, cancelFunc := context.WithTimeout(context.Background(), time.Second*5) defer cancelFunc() - c := NewCache(strategy) tc.before(t) - val := c.LPop(ctx, tc.key) + val := cache.LPop(ctx, tc.key) result, err := val.String() assert.Equal(t, tc.wantVal, result) assert.Equal(t, tc.wantErr, err) @@ -600,9 +581,7 @@ func TestCache_SAdd(t *testing.T) { onEvicted := func(key string, value any) { evictCounter++ } - strategy := NewLRU[string, any]( - WithCapacity[string, any](5), - WithCallback[string, any](onEvicted)) + cache := NewCache(WithCapacity(5), WithCallback(onEvicted)) testCase := []struct { name string @@ -618,7 +597,7 @@ func TestCache_SAdd(t *testing.T) { name: "sadd value", before: func(t *testing.T) {}, after: func(t *testing.T) { - assert.Equal(t, true, strategy.Remove("test")) + assert.Equal(t, true, cache.remove("test")) }, key: "test", val: []any{"hello ecache", "hello world"}, @@ -630,10 +609,10 @@ func TestCache_SAdd(t *testing.T) { s := set.NewMapSet[any](8) s.Add("hello world") - assert.Equal(t, false, strategy.Add("test", s)) + assert.Equal(t, false, cache.add("test", s)) }, after: func(t *testing.T) { - assert.Equal(t, true, strategy.Remove("test")) + assert.Equal(t, true, cache.remove("test")) }, key: "test", val: []any{"hello ecache"}, @@ -642,10 +621,10 @@ func TestCache_SAdd(t *testing.T) { { name: "sadd value type err", before: func(t *testing.T) { - assert.Equal(t, false, strategy.Add("test", "string")) + assert.Equal(t, false, cache.add("test", "string")) }, after: func(t *testing.T) { - assert.Equal(t, true, strategy.Remove("test")) + assert.Equal(t, true, cache.remove("test")) }, key: "test", val: []any{"hello"}, @@ -657,10 +636,9 @@ func TestCache_SAdd(t *testing.T) { t.Run(tc.name, func(t *testing.T) { ctx, cancelFunc := context.WithTimeout(context.Background(), time.Second*5) defer cancelFunc() - c := NewCache(strategy) tc.before(t) - val, err := c.SAdd(ctx, tc.key, tc.val...) + val, err := cache.SAdd(ctx, tc.key, tc.val...) assert.Equal(t, tc.wantVal, val) assert.Equal(t, tc.wantErr, err) tc.after(t) @@ -673,9 +651,7 @@ func TestCache_SRem(t *testing.T) { onEvicted := func(key string, value any) { evictCounter++ } - strategy := NewLRU[string, any]( - WithCapacity[string, any](5), - WithCallback[string, any](onEvicted)) + cache := NewCache(WithCapacity(5), WithCallback(onEvicted)) testCase := []struct { name string @@ -696,10 +672,10 @@ func TestCache_SRem(t *testing.T) { s.Add("hello world") s.Add("hello ecache") - assert.Equal(t, false, strategy.Add("test", s)) + assert.Equal(t, false, cache.add("test", s)) }, after: func(t *testing.T) { - assert.Equal(t, true, strategy.Remove("test")) + assert.Equal(t, true, cache.remove("test")) }, key: "test", val: []any{"hello world"}, @@ -711,10 +687,10 @@ func TestCache_SRem(t *testing.T) { s := set.NewMapSet[any](8) s.Add("hello world") - assert.Equal(t, false, strategy.Add("test", s)) + assert.Equal(t, false, cache.add("test", s)) }, after: func(t *testing.T) { - assert.Equal(t, true, strategy.Remove("test")) + assert.Equal(t, true, cache.remove("test")) }, key: "test", val: []any{"hello ecache"}, @@ -731,10 +707,10 @@ func TestCache_SRem(t *testing.T) { { name: "srem value type error", before: func(t *testing.T) { - assert.Equal(t, false, strategy.Add("test", int64(1))) + assert.Equal(t, false, cache.add("test", int64(1))) }, after: func(t *testing.T) { - assert.Equal(t, true, strategy.Remove("test")) + assert.Equal(t, true, cache.remove("test")) }, key: "test", val: []any{"hello world"}, @@ -747,10 +723,9 @@ func TestCache_SRem(t *testing.T) { defer tc.after(t) ctx, cancelFunc := context.WithTimeout(context.Background(), time.Second*5) defer cancelFunc() - c := NewCache(strategy) tc.before(t) - val, err := c.SRem(ctx, tc.key, tc.val...) + val, err := cache.SRem(ctx, tc.key, tc.val...) assert.Equal(t, tc.wantErr, err) assert.Equal(t, tc.wantVal, val) }) @@ -762,9 +737,7 @@ func TestCache_IncrBy(t *testing.T) { onEvicted := func(key string, value any) { evictCounter++ } - strategy := NewLRU[string, any]( - WithCapacity[string, any](5), - WithCallback[string, any](onEvicted)) + cache := NewCache(WithCapacity(5), WithCallback(onEvicted)) testCase := []struct { name string @@ -780,7 +753,7 @@ func TestCache_IncrBy(t *testing.T) { name: "incrby value", before: func(t *testing.T) {}, after: func(t *testing.T) { - assert.Equal(t, true, strategy.Remove("test")) + assert.Equal(t, true, cache.remove("test")) }, key: "test", val: 1, @@ -789,10 +762,10 @@ func TestCache_IncrBy(t *testing.T) { { name: "incrby value add", before: func(t *testing.T) { - assert.Equal(t, false, strategy.Add("test", int64(1))) + assert.Equal(t, false, cache.add("test", int64(1))) }, after: func(t *testing.T) { - assert.Equal(t, true, strategy.Remove("test")) + assert.Equal(t, true, cache.remove("test")) }, key: "test", val: 1, @@ -801,10 +774,10 @@ func TestCache_IncrBy(t *testing.T) { { name: "incrby value type error", before: func(t *testing.T) { - assert.Equal(t, false, strategy.Add("test", 12.62)) + assert.Equal(t, false, cache.add("test", 12.62)) }, after: func(t *testing.T) { - assert.Equal(t, true, strategy.Remove("test")) + assert.Equal(t, true, cache.remove("test")) }, key: "test", val: 1, @@ -816,10 +789,9 @@ func TestCache_IncrBy(t *testing.T) { t.Run(tc.name, func(t *testing.T) { ctx, cancelFunc := context.WithTimeout(context.Background(), time.Second*5) defer cancelFunc() - c := NewCache(strategy) tc.before(t) - result, err := c.IncrBy(ctx, tc.key, tc.val) + result, err := cache.IncrBy(ctx, tc.key, tc.val) assert.Equal(t, tc.wantVal, result) assert.Equal(t, tc.wantErr, err) tc.after(t) @@ -832,9 +804,7 @@ func TestCache_DecrBy(t *testing.T) { onEvicted := func(key string, value any) { evictCounter++ } - strategy := NewLRU[string, any]( - WithCapacity[string, any](5), - WithCallback[string, any](onEvicted)) + cache := NewCache(WithCapacity(5), WithCallback(onEvicted)) testCase := []struct { name string @@ -850,7 +820,7 @@ func TestCache_DecrBy(t *testing.T) { name: "decrby value", before: func(t *testing.T) {}, after: func(t *testing.T) { - assert.Equal(t, true, strategy.Remove("test")) + assert.Equal(t, true, cache.remove("test")) }, key: "test", val: 1, @@ -859,10 +829,10 @@ func TestCache_DecrBy(t *testing.T) { { name: "decrby old value", before: func(t *testing.T) { - assert.Equal(t, false, strategy.Add("test", int64(3))) + assert.Equal(t, false, cache.add("test", int64(3))) }, after: func(t *testing.T) { - assert.Equal(t, true, strategy.Remove("test")) + assert.Equal(t, true, cache.remove("test")) }, key: "test", val: 2, @@ -871,10 +841,10 @@ func TestCache_DecrBy(t *testing.T) { { name: "decrby value type error", before: func(t *testing.T) { - assert.Equal(t, false, strategy.Add("test", 3.156)) + assert.Equal(t, false, cache.add("test", 3.156)) }, after: func(t *testing.T) { - assert.Equal(t, true, strategy.Remove("test")) + assert.Equal(t, true, cache.remove("test")) }, key: "test", val: 1, @@ -886,10 +856,9 @@ func TestCache_DecrBy(t *testing.T) { t.Run(tc.name, func(t *testing.T) { ctx, cancelFunc := context.WithTimeout(context.Background(), time.Second*5) defer cancelFunc() - c := NewCache(strategy) tc.before(t) - val, err := c.DecrBy(ctx, tc.key, tc.val) + val, err := cache.DecrBy(ctx, tc.key, tc.val) assert.Equal(t, tc.wantVal, val) assert.Equal(t, tc.wantErr, err) tc.after(t) @@ -902,9 +871,7 @@ func TestCache_IncrByFloat(t *testing.T) { onEvicted := func(key string, value any) { evictCounter++ } - strategy := NewLRU[string, any]( - WithCapacity[string, any](5), - WithCallback[string, any](onEvicted)) + cache := NewCache(WithCapacity(5), WithCallback(onEvicted)) testCase := []struct { name string @@ -920,7 +887,7 @@ func TestCache_IncrByFloat(t *testing.T) { name: "incrbyfloat value", before: func(t *testing.T) {}, after: func(t *testing.T) { - assert.Equal(t, true, strategy.Remove("test")) + assert.Equal(t, true, cache.remove("test")) }, key: "test", val: 2.0, @@ -929,10 +896,10 @@ func TestCache_IncrByFloat(t *testing.T) { { name: "incrbyfloat decr value", before: func(t *testing.T) { - assert.Equal(t, false, strategy.Add("test", 3.1)) + assert.Equal(t, false, cache.add("test", 3.1)) }, after: func(t *testing.T) { - assert.Equal(t, true, strategy.Remove("test")) + assert.Equal(t, true, cache.remove("test")) }, key: "test", val: -2.0, @@ -941,10 +908,10 @@ func TestCache_IncrByFloat(t *testing.T) { { name: "incrbyfloat value type error", before: func(t *testing.T) { - assert.Equal(t, false, strategy.Add("test", "hello")) + assert.Equal(t, false, cache.add("test", "hello")) }, after: func(t *testing.T) { - assert.Equal(t, true, strategy.Remove("test")) + assert.Equal(t, true, cache.remove("test")) }, key: "test", val: 10, @@ -956,28 +923,12 @@ func TestCache_IncrByFloat(t *testing.T) { t.Run(tc.name, func(t *testing.T) { ctx, cancelFunc := context.WithTimeout(context.Background(), time.Second*5) defer cancelFunc() - c := NewCache(strategy) tc.before(t) - val, err := c.IncrByFloat(ctx, tc.key, tc.val) + val, err := cache.IncrByFloat(ctx, tc.key, tc.val) assert.Equal(t, tc.wantVal, val) assert.Equal(t, tc.wantErr, err) tc.after(t) }) } } - -func newCache() (ecache.Cache, error) { - strategy := newLRUStrategy(10) - return NewCache(strategy), nil -} - -func newLRUStrategy(size int) *LRU[string, any] { - evictCounter := 0 - onEvicted := func(key string, value any) { - evictCounter++ - } - return NewLRU[string, any]( - WithCapacity[string, any](size), - WithCallback[string, any](onEvicted)) -} diff --git a/memory/lru/list.go b/memory/lru/list.go index eb098c9..5a326e8 100644 --- a/memory/lru/list.go +++ b/memory/lru/list.go @@ -14,68 +14,68 @@ package lru -type Element[T any] struct { +type element[T any] struct { Value T - list *LinkedList[T] - next, prev *Element[T] + list *linkedList[T] + next, prev *element[T] } -func (e *Element[T]) Next() *Element[T] { +func (e *element[T]) Next() *element[T] { if n := e.next; e.list != nil && n != &e.list.root { return n } return nil } -func (e *Element[T]) Prev() *Element[T] { +func (e *element[T]) Prev() *element[T] { if p := e.prev; e.list != nil && p != &e.list.root { return p } return nil } -type LinkedList[T any] struct { - root Element[T] +type linkedList[T any] struct { + root element[T] len int } -func NewLinkedList[T any]() *LinkedList[T] { - l := &LinkedList[T]{} +func newLinkedList[T any]() *linkedList[T] { + l := &linkedList[T]{} return l.Init() } -func (l *LinkedList[T]) Init() *LinkedList[T] { +func (l *linkedList[T]) Init() *linkedList[T] { l.root.next = &l.root l.root.prev = &l.root l.len = 0 return l } -func (l *LinkedList[T]) Len() int { +func (l *linkedList[T]) Len() int { return l.len } -func (l *LinkedList[T]) Front() *Element[T] { +func (l *linkedList[T]) Front() *element[T] { if l.len == 0 { return nil } return l.root.next } -func (l *LinkedList[T]) Back() *Element[T] { +func (l *linkedList[T]) Back() *element[T] { if l.len == 0 { return nil } return l.root.prev } -func (l *LinkedList[T]) lazyInit() { +func (l *linkedList[T]) lazyInit() { if l.root.next == nil { l.Init() } } -func (l *LinkedList[T]) insert(e, at *Element[T]) *Element[T] { +func (l *linkedList[T]) insert(e, at *element[T]) *element[T] { e.prev = at e.next = at.next e.prev.next = e @@ -85,11 +85,11 @@ func (l *LinkedList[T]) insert(e, at *Element[T]) *Element[T] { return e } -func (l *LinkedList[T]) insertValue(v T, at *Element[T]) *Element[T] { - return l.insert(&Element[T]{Value: v}, at) +func (l *linkedList[T]) insertValue(v T, at *element[T]) *element[T] { + return l.insert(&element[T]{Value: v}, at) } -func (l *LinkedList[T]) remove(e *Element[T]) { +func (l *linkedList[T]) remove(e *element[T]) { e.prev.next = e.next e.next.prev = e.prev e.next = nil @@ -98,7 +98,7 @@ func (l *LinkedList[T]) remove(e *Element[T]) { l.len-- } -func (l *LinkedList[T]) move(e, at *Element[T]) { +func (l *linkedList[T]) move(e, at *element[T]) { if e == at { return } @@ -111,66 +111,66 @@ func (l *LinkedList[T]) move(e, at *Element[T]) { e.next.prev = e } -func (l *LinkedList[T]) Remove(e *Element[T]) any { +func (l *linkedList[T]) Remove(e *element[T]) any { if e.list == l { l.remove(e) } return e.Value } -func (l *LinkedList[T]) PushFront(v T) *Element[T] { +func (l *linkedList[T]) PushFront(v T) *element[T] { l.lazyInit() return l.insertValue(v, &l.root) } -func (l *LinkedList[T]) PushBack(v T) *Element[T] { +func (l *linkedList[T]) PushBack(v T) *element[T] { l.lazyInit() return l.insertValue(v, l.root.prev) } -func (l *LinkedList[T]) MoveToFront(e *Element[T]) { +func (l *linkedList[T]) MoveToFront(e *element[T]) { if e.list != l || l.root.next == e { return } l.move(e, &l.root) } -func (l *LinkedList[T]) MoveToBack(e *Element[T]) { +func (l *linkedList[T]) MoveToBack(e *element[T]) { if e.list != l || l.root.prev == e { return } l.move(e, l.root.prev) } -func (l *LinkedList[T]) MoveBefore(e, mark *Element[T]) { +func (l *linkedList[T]) MoveBefore(e, mark *element[T]) { if e.list != l || e == mark || mark.list != l { return } l.move(e, mark.prev) } -func (l *LinkedList[T]) MoveAfter(e, mark *Element[T]) { +func (l *linkedList[T]) MoveAfter(e, mark *element[T]) { if e.list != l || e == mark || mark.list != l { return } l.move(e, mark) } -func (l *LinkedList[T]) InsertBefore(v T, mark *Element[T]) *Element[T] { +func (l *linkedList[T]) InsertBefore(v T, mark *element[T]) *element[T] { if mark.list != l { return nil } return l.insertValue(v, mark.prev) } -func (l *LinkedList[T]) InsertAfter(v T, mark *Element[T]) *Element[T] { +func (l *linkedList[T]) InsertAfter(v T, mark *element[T]) *element[T] { if mark.list != l { return nil } return l.insertValue(v, mark) } -func (l *LinkedList[T]) PushBackList(other *LinkedList[T]) { +func (l *linkedList[T]) PushBackList(other *linkedList[T]) { l.lazyInit() e := other.Front() for i := other.Len(); i > 0; i-- { @@ -179,7 +179,7 @@ func (l *LinkedList[T]) PushBackList(other *LinkedList[T]) { } } -func (l *LinkedList[T]) PushFrontList(other *LinkedList[T]) { +func (l *linkedList[T]) PushFrontList(other *linkedList[T]) { l.lazyInit() for i, e := other.Len(), other.Back(); i > 0; i, e = i-1, e.Prev() { l.insertValue(e.Value, &l.root) diff --git a/memory/lru/list_test.go b/memory/lru/list_test.go index 69887ab..f9f0779 100644 --- a/memory/lru/list_test.go +++ b/memory/lru/list_test.go @@ -21,7 +21,7 @@ import ( func Example() { // Create a new list and put some numbers in it. - l := NewLinkedList[int]() + l := newLinkedList[int]() e4 := l.PushBack(4) e1 := l.PushFront(1) l.InsertBefore(3, e4) @@ -39,7 +39,7 @@ func Example() { // 4 } -func checkLinkedListLen[T any](t *testing.T, l *LinkedList[T], len int) bool { +func checkLinkedListLen[T any](t *testing.T, l *linkedList[T], len int) bool { if n := l.Len(); n != len { t.Errorf("l.Len() = %d, want %d", n, len) return false @@ -47,7 +47,7 @@ func checkLinkedListLen[T any](t *testing.T, l *LinkedList[T], len int) bool { return true } -func checkLinkedListPointers[T any](t *testing.T, l *LinkedList[T], es []*Element[T]) { +func checkLinkedListPointers[T any](t *testing.T, l *linkedList[T], es []*element[T]) { root := &l.root if !checkLinkedListLen[T](t, l, len(es)) { @@ -66,7 +66,7 @@ func checkLinkedListPointers[T any](t *testing.T, l *LinkedList[T], es []*Elemen // check internal and external prev/next connections for i, e := range es { prev := root - Prev := (*Element[T])(nil) + Prev := (*element[T])(nil) if i > 0 { prev = es[i-1] Prev = prev @@ -79,7 +79,7 @@ func checkLinkedListPointers[T any](t *testing.T, l *LinkedList[T], es []*Elemen } next := root - Next := (*Element[T])(nil) + Next := (*element[T])(nil) if i < len(es)-1 { next = es[i+1] Next = next @@ -94,63 +94,63 @@ func checkLinkedListPointers[T any](t *testing.T, l *LinkedList[T], es []*Elemen } func TestLinkedList(t *testing.T) { - l := NewLinkedList[any]() - checkLinkedListPointers(t, l, []*Element[any]{}) + l := newLinkedList[any]() + checkLinkedListPointers(t, l, []*element[any]{}) // Single element LinkList e := l.PushFront("a") - checkLinkedListPointers(t, l, []*Element[any]{e}) + checkLinkedListPointers(t, l, []*element[any]{e}) l.MoveToFront(e) - checkLinkedListPointers(t, l, []*Element[any]{e}) + checkLinkedListPointers(t, l, []*element[any]{e}) l.MoveToBack(e) - checkLinkedListPointers(t, l, []*Element[any]{e}) + checkLinkedListPointers(t, l, []*element[any]{e}) l.Remove(e) - checkLinkedListPointers(t, l, []*Element[any]{}) + checkLinkedListPointers(t, l, []*element[any]{}) // Bigger LinkList e2 := l.PushFront(2) e1 := l.PushFront(1) e3 := l.PushBack(3) e4 := l.PushBack("banana") - checkLinkedListPointers(t, l, []*Element[any]{e1, e2, e3, e4}) + checkLinkedListPointers(t, l, []*element[any]{e1, e2, e3, e4}) l.Remove(e2) - checkLinkedListPointers(t, l, []*Element[any]{e1, e3, e4}) + checkLinkedListPointers(t, l, []*element[any]{e1, e3, e4}) l.MoveToFront(e3) // move from middle - checkLinkedListPointers(t, l, []*Element[any]{e3, e1, e4}) + checkLinkedListPointers(t, l, []*element[any]{e3, e1, e4}) l.MoveToFront(e1) l.MoveToBack(e3) // move from middle - checkLinkedListPointers(t, l, []*Element[any]{e1, e4, e3}) + checkLinkedListPointers(t, l, []*element[any]{e1, e4, e3}) l.MoveToFront(e3) // move from back - checkLinkedListPointers(t, l, []*Element[any]{e3, e1, e4}) + checkLinkedListPointers(t, l, []*element[any]{e3, e1, e4}) l.MoveToFront(e3) // should be no-op - checkLinkedListPointers(t, l, []*Element[any]{e3, e1, e4}) + checkLinkedListPointers(t, l, []*element[any]{e3, e1, e4}) l.MoveToBack(e3) // move from front - checkLinkedListPointers(t, l, []*Element[any]{e1, e4, e3}) + checkLinkedListPointers(t, l, []*element[any]{e1, e4, e3}) l.MoveToBack(e3) // should be no-op - checkLinkedListPointers(t, l, []*Element[any]{e1, e4, e3}) + checkLinkedListPointers(t, l, []*element[any]{e1, e4, e3}) e2 = l.InsertBefore(2, e1) // insert before front - checkLinkedListPointers(t, l, []*Element[any]{e2, e1, e4, e3}) + checkLinkedListPointers(t, l, []*element[any]{e2, e1, e4, e3}) l.Remove(e2) e2 = l.InsertBefore(2, e4) // insert before middle - checkLinkedListPointers(t, l, []*Element[any]{e1, e2, e4, e3}) + checkLinkedListPointers(t, l, []*element[any]{e1, e2, e4, e3}) l.Remove(e2) e2 = l.InsertBefore(2, e3) // insert before back - checkLinkedListPointers(t, l, []*Element[any]{e1, e4, e2, e3}) + checkLinkedListPointers(t, l, []*element[any]{e1, e4, e2, e3}) l.Remove(e2) e2 = l.InsertAfter(2, e1) // insert after front - checkLinkedListPointers(t, l, []*Element[any]{e1, e2, e4, e3}) + checkLinkedListPointers(t, l, []*element[any]{e1, e2, e4, e3}) l.Remove(e2) e2 = l.InsertAfter(2, e4) // insert after middle - checkLinkedListPointers(t, l, []*Element[any]{e1, e4, e2, e3}) + checkLinkedListPointers(t, l, []*element[any]{e1, e4, e2, e3}) l.Remove(e2) e2 = l.InsertAfter(2, e3) // insert after back - checkLinkedListPointers(t, l, []*Element[any]{e1, e4, e3, e2}) + checkLinkedListPointers(t, l, []*element[any]{e1, e4, e3, e2}) l.Remove(e2) // Check standard iteration. @@ -165,15 +165,15 @@ func TestLinkedList(t *testing.T) { } // Clear all elements by iterating - var next *Element[any] + var next *element[any] for e := l.Front(); e != nil; e = next { next = e.Next() l.Remove(e) } - checkLinkedListPointers(t, l, []*Element[any]{}) + checkLinkedListPointers(t, l, []*element[any]{}) } -func checkLinkedList[T int](t *testing.T, l *LinkedList[T], es []any) { +func checkLinkedList[T int](t *testing.T, l *linkedList[T], es []any) { if !checkLinkedListLen[T](t, l, len(es)) { return } @@ -189,8 +189,8 @@ func checkLinkedList[T int](t *testing.T, l *LinkedList[T], es []any) { } func TestExtendingEle(t *testing.T) { - l1 := NewLinkedList[int]() - l2 := NewLinkedList[int]() + l1 := newLinkedList[int]() + l2 := newLinkedList[int]() l1.PushBack(1) l1.PushBack(2) @@ -199,13 +199,13 @@ func TestExtendingEle(t *testing.T) { l2.PushBack(4) l2.PushBack(5) - l3 := NewLinkedList[int]() + l3 := newLinkedList[int]() l3.PushBackList(l1) checkLinkedList(t, l3, []any{1, 2, 3}) l3.PushBackList(l2) checkLinkedList(t, l3, []any{1, 2, 3, 4, 5}) - l3 = NewLinkedList[int]() + l3 = newLinkedList[int]() l3.PushFrontList(l2) checkLinkedList(t, l3, []any{4, 5}) l3.PushFrontList(l1) @@ -214,19 +214,19 @@ func TestExtendingEle(t *testing.T) { checkLinkedList(t, l1, []any{1, 2, 3}) checkLinkedList(t, l2, []any{4, 5}) - l3 = NewLinkedList[int]() + l3 = newLinkedList[int]() l3.PushBackList(l1) checkLinkedList(t, l3, []any{1, 2, 3}) l3.PushBackList(l3) checkLinkedList(t, l3, []any{1, 2, 3, 1, 2, 3}) - l3 = NewLinkedList[int]() + l3 = newLinkedList[int]() l3.PushFrontList(l1) checkLinkedList(t, l3, []any{1, 2, 3}) l3.PushFrontList(l3) checkLinkedList(t, l3, []any{1, 2, 3, 1, 2, 3}) - l3 = NewLinkedList[int]() + l3 = newLinkedList[int]() l1.PushBackList(l3) checkLinkedList(t, l1, []any{1, 2, 3}) l1.PushFrontList(l3) @@ -234,23 +234,23 @@ func TestExtendingEle(t *testing.T) { } func TestRemoveEle(t *testing.T) { - l := NewLinkedList[int]() + l := newLinkedList[int]() e1 := l.PushBack(1) e2 := l.PushBack(2) - checkLinkedListPointers(t, l, []*Element[int]{e1, e2}) + checkLinkedListPointers(t, l, []*element[int]{e1, e2}) e := l.Front() l.Remove(e) - checkLinkedListPointers(t, l, []*Element[int]{e2}) + checkLinkedListPointers(t, l, []*element[int]{e2}) l.Remove(e) - checkLinkedListPointers(t, l, []*Element[int]{e2}) + checkLinkedListPointers(t, l, []*element[int]{e2}) } func TestIssue4103Ele(t *testing.T) { - l1 := NewLinkedList[int]() + l1 := newLinkedList[int]() l1.PushBack(1) l1.PushBack(2) - l2 := NewLinkedList[int]() + l2 := newLinkedList[int]() l2.PushBack(3) l2.PushBack(4) @@ -267,7 +267,7 @@ func TestIssue4103Ele(t *testing.T) { } func TestIssue6349Ele(t *testing.T) { - l := NewLinkedList[int]() + l := newLinkedList[int]() l.PushBack(1) l.PushBack(2) @@ -285,79 +285,79 @@ func TestIssue6349Ele(t *testing.T) { } func TestMoveEle(t *testing.T) { - l := NewLinkedList[int]() + l := newLinkedList[int]() e1 := l.PushBack(1) e2 := l.PushBack(2) e3 := l.PushBack(3) e4 := l.PushBack(4) l.MoveAfter(e3, e3) - checkLinkedListPointers(t, l, []*Element[int]{e1, e2, e3, e4}) + checkLinkedListPointers(t, l, []*element[int]{e1, e2, e3, e4}) l.MoveBefore(e2, e2) - checkLinkedListPointers(t, l, []*Element[int]{e1, e2, e3, e4}) + checkLinkedListPointers(t, l, []*element[int]{e1, e2, e3, e4}) l.MoveAfter(e3, e2) - checkLinkedListPointers(t, l, []*Element[int]{e1, e2, e3, e4}) + checkLinkedListPointers(t, l, []*element[int]{e1, e2, e3, e4}) l.MoveBefore(e2, e3) - checkLinkedListPointers(t, l, []*Element[int]{e1, e2, e3, e4}) + checkLinkedListPointers(t, l, []*element[int]{e1, e2, e3, e4}) l.MoveBefore(e2, e4) - checkLinkedListPointers(t, l, []*Element[int]{e1, e3, e2, e4}) + checkLinkedListPointers(t, l, []*element[int]{e1, e3, e2, e4}) e2, e3 = e3, e2 l.MoveBefore(e4, e1) - checkLinkedListPointers(t, l, []*Element[int]{e4, e1, e2, e3}) + checkLinkedListPointers(t, l, []*element[int]{e4, e1, e2, e3}) e1, e2, e3, e4 = e4, e1, e2, e3 l.MoveAfter(e4, e1) - checkLinkedListPointers(t, l, []*Element[int]{e1, e4, e2, e3}) + checkLinkedListPointers(t, l, []*element[int]{e1, e4, e2, e3}) e2, e3, e4 = e4, e2, e3 l.MoveAfter(e2, e3) - checkLinkedListPointers(t, l, []*Element[int]{e1, e3, e2, e4}) + checkLinkedListPointers(t, l, []*element[int]{e1, e3, e2, e4}) } func TestZeroLinkedList(t *testing.T) { - var l1 = new(LinkedList[int]) + var l1 = new(linkedList[int]) l1.PushFront(1) checkLinkedList(t, l1, []any{1}) - var l2 = new(LinkedList[int]) + var l2 = new(linkedList[int]) l2.PushBack(1) checkLinkedList(t, l2, []any{1}) - var l3 = new(LinkedList[int]) + var l3 = new(linkedList[int]) l3.PushFrontList(l1) checkLinkedList(t, l3, []any{1}) - var l4 = new(LinkedList[int]) + var l4 = new(linkedList[int]) l4.PushBackList(l2) checkLinkedList(t, l4, []any{1}) } func TestInsertBeforeUnknownMarkEle(t *testing.T) { - var l LinkedList[int] + var l linkedList[int] l.PushBack(1) l.PushBack(2) l.PushBack(3) - l.InsertBefore(1, new(Element[int])) + l.InsertBefore(1, new(element[int])) checkLinkedList(t, &l, []any{1, 2, 3}) } func TestInsertAfterUnknownMarkEle(t *testing.T) { - var l LinkedList[int] + var l linkedList[int] l.PushBack(1) l.PushBack(2) l.PushBack(3) - l.InsertAfter(1, new(Element[int])) + l.InsertAfter(1, new(element[int])) checkLinkedList(t, &l, []any{1, 2, 3}) } func TestMoveUnknownMarkEle(t *testing.T) { - var l1 LinkedList[int] + var l1 linkedList[int] e1 := l1.PushBack(1) - var l2 LinkedList[int] + var l2 linkedList[int] e2 := l2.PushBack(2) l1.MoveAfter(e1, e2) diff --git a/memory/lru/lru.go b/memory/lru/lru.go deleted file mode 100644 index 770d062..0000000 --- a/memory/lru/lru.go +++ /dev/null @@ -1,295 +0,0 @@ -// Copyright 2023 ecodeclub -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package lru - -import ( - "sync" - "time" -) - -const ( - defaultCapacity = 100 -) - -type entry[K comparable, V any] struct { - key K - value V - expiresAt time.Time -} - -func (e entry[K, V]) isExpired() bool { - return e.expiresAt.Before(time.Now()) -} - -func (e entry[K, V]) existExpiration() bool { - return !e.expiresAt.Equal(time.Time{}) -} - -type EvictCallback[K comparable, V any] func(key K, value V) - -type Option[K comparable, V any] func(l *LRU[K, V]) - -func WithCallback[K comparable, V any](callback func(k K, v V)) Option[K, V] { - return func(l *LRU[K, V]) { - l.callback = callback - } -} - -func WithCapacity[K comparable, V any](capacity int) Option[K, V] { - return func(l *LRU[K, V]) { - l.capacity = capacity - } -} - -type LRU[K comparable, V any] struct { - lock sync.RWMutex - capacity int - list *LinkedList[entry[K, V]] - data map[K]*Element[entry[K, V]] - callback EvictCallback[K, V] -} - -func NewLRU[K comparable, V any](options ...Option[K, V]) *LRU[K, V] { - res := &LRU[K, V]{ - list: NewLinkedList[entry[K, V]](), - data: make(map[K]*Element[entry[K, V]], 16), - capacity: defaultCapacity, - } - for _, opt := range options { - opt(res) - } - return res -} - -func (l *LRU[K, V]) Purge() { - l.lock.Lock() - defer l.lock.Unlock() - for k, v := range l.data { - if l.callback != nil { - l.callback(v.Value.key, v.Value.value) - } - l.delete(k) - } - l.list.Init() -} - -func (l *LRU[K, V]) pushEntry(key K, ent entry[K, V]) (evicted bool) { - if elem, ok := l.data[key]; ok { - elem.Value = ent - l.list.MoveToFront(elem) - return false - } - elem := l.list.PushFront(ent) - l.data[key] = elem - evict := l.len() > l.capacity - if evict { - l.removeOldest() - } - return evict -} - -func (l *LRU[K, V]) addTTL(key K, value V, expiration time.Duration) (evicted bool) { - ent := entry[K, V]{key: key, value: value, - expiresAt: time.Now().Add(expiration)} - return l.pushEntry(key, ent) -} - -func (l *LRU[K, V]) add(key K, value V) (evicted bool) { - ent := entry[K, V]{key: key, value: value} - return l.pushEntry(key, ent) -} - -func (l *LRU[K, V]) AddTTL(key K, value V, expiration time.Duration) (evicted bool) { - l.lock.Lock() - defer l.lock.Unlock() - return l.addTTL(key, value, expiration) -} - -func (l *LRU[K, V]) Add(key K, value V) (evicted bool) { - l.lock.Lock() - defer l.lock.Unlock() - return l.add(key, value) -} - -func (l *LRU[K, V]) Get(key K) (value V, ok bool) { - l.lock.Lock() - defer l.lock.Unlock() - if elem, exist := l.data[key]; exist { - entry := elem.Value - if entry.existExpiration() && entry.isExpired() { - l.removeElement(elem) - return - } - l.list.MoveToFront(elem) - return entry.value, true - } - return -} - -func (l *LRU[K, V]) peek(key K) (value V, ok bool) { - if elem, exist := l.data[key]; exist { - entry := elem.Value - if entry.existExpiration() && entry.isExpired() { - l.removeElement(elem) - return - } - return entry.value, true - } - return -} - -func (l *LRU[K, V]) Peek(key K) (value V, ok bool) { - l.lock.Lock() - defer l.lock.Unlock() - return l.peek(key) -} - -func (l *LRU[K, V]) GetOldest() (key K, value V, ok bool) { - l.lock.Lock() - defer l.lock.Unlock() - elem := l.list.Back() - for elem != nil { - entry := elem.Value - if !entry.existExpiration() || !entry.isExpired() { - return entry.key, entry.value, true - } - l.removeElement(elem) - elem = l.list.Back() - } - return -} - -func (l *LRU[K, V]) RemoveOldest() (key K, value V, ok bool) { - l.lock.Lock() - defer l.lock.Unlock() - if elem := l.list.Back(); elem != nil { - l.removeElement(elem) - return elem.Value.key, elem.Value.value, true - } - return -} - -func (l *LRU[K, V]) removeOldest() { - if ent := l.list.Back(); ent != nil { - l.removeElement(ent) - } -} - -func (l *LRU[K, V]) removeElement(elem *Element[entry[K, V]]) { - l.list.Remove(elem) - entry := elem.Value - l.delete(entry.key) - if l.callback != nil { - l.callback(entry.key, entry.value) - } -} - -func (l *LRU[K, V]) Remove(key K) (present bool) { - l.lock.Lock() - defer l.lock.Unlock() - if elem, ok := l.data[key]; ok { - l.removeElement(elem) - if elem.Value.existExpiration() && elem.Value.isExpired() { - return false - } - return true - } - return false -} - -func (l *LRU[K, V]) Resize(size int) (evicted int) { - l.lock.Lock() - defer l.lock.Unlock() - diff := l.len() - size - if diff < 0 { - diff = 0 - } - for i := 0; i < diff; i++ { - l.removeOldest() - } - l.capacity = size - return diff -} - -func (l *LRU[K, V]) contains(key K) (ok bool) { - elem, ok := l.data[key] - if ok { - if elem.Value.existExpiration() && elem.Value.isExpired() { - l.removeElement(elem) - return false - } - } - return ok -} - -func (l *LRU[K, V]) Contains(key K) (ok bool) { - l.lock.Lock() - defer l.lock.Unlock() - return l.contains(key) -} - -func (l *LRU[K, V]) delete(key K) { - delete(l.data, key) -} - -func (l *LRU[K, V]) len() int { - var length int - for elem := l.list.Back(); elem != nil; elem = elem.Prev() { - if elem.Value.existExpiration() && elem.Value.isExpired() { - l.removeElement(elem) - continue - } - length++ - } - return length -} - -func (l *LRU[K, V]) Len() int { - l.lock.Lock() - defer l.lock.Unlock() - return l.len() -} - -func (l *LRU[K, V]) Keys() []K { - l.lock.Lock() - defer l.lock.Unlock() - keys := make([]K, l.list.Len()) - i := 0 - for elem := l.list.Back(); elem != nil; elem = elem.Prev() { - if elem.Value.existExpiration() && elem.Value.isExpired() { - l.removeElement(elem) - continue - } - keys[i] = elem.Value.key - i++ - } - return keys -} - -func (l *LRU[K, V]) Values() []V { - l.lock.Lock() - defer l.lock.Unlock() - values := make([]V, l.list.Len()) - i := 0 - for elem := l.list.Back(); elem != nil; elem = elem.Prev() { - if elem.Value.existExpiration() && elem.Value.isExpired() { - l.removeElement(elem) - continue - } - values[i] = elem.Value.value - i++ - } - return values -} diff --git a/memory/lru/lru_test.go b/memory/lru/lru_test.go deleted file mode 100644 index e0dda4f..0000000 --- a/memory/lru/lru_test.go +++ /dev/null @@ -1,341 +0,0 @@ -// Copyright 2023 ecodeclub -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package lru - -import ( - "reflect" - "testing" - "time" -) - -func TestLRUTTL(t *testing.T) { - evictCounter := 0 - onEvicted := func(k int, v int) { - if k != v { - t.Fatalf("Evict values not equal (%v!=%v)", k, v) - } - evictCounter++ - } - l := NewLRU[int, int]( - WithCapacity[int, int](128), WithCallback[int, int](onEvicted)) - - for i := 0; i < 256; i++ { - l.AddTTL(i, i, 2*time.Second) - } - if l.Len() != 128 { - t.Fatalf("bad len: %v", l.Len()) - } - - if evictCounter != 128 { - t.Fatalf("bad evict count: %v", evictCounter) - } - - for i, k := range l.Keys() { - if v, ok := l.Get(k); !ok || v != k || v != i+128 { - t.Fatalf("bad key: %v", k) - } - } - for i, v := range l.Values() { - if v != i+128 { - t.Fatalf("bad value: %v", v) - } - } - for i := 0; i < 128; i++ { - if _, ok := l.Get(i); ok { - t.Fatalf("should be evicted") - } - } - for i := 128; i < 256; i++ { - if _, ok := l.Get(i); !ok { - t.Fatalf("should not be evicted") - } - } - for i := 128; i < 192; i++ { - if ok := l.Remove(i); !ok { - t.Fatalf("should be contained") - } - if ok := l.Remove(i); ok { - t.Fatalf("should not be contained") - } - if _, ok := l.Get(i); ok { - t.Fatalf("should be deleted") - } - } - - l.Get(192) // expect 192 to be last key in l.Keys() - - for i, k := range l.Keys() { - if (i < 63 && k != i+193) || (i == 63 && k != 192) { - t.Fatalf("out of order key: %v", k) - } - } - - time.Sleep(3 * time.Second) - if l.Len() != 0 { - t.Fatalf("bad len: %v", l.Len()) - } - if _, ok := l.Get(200); ok { - t.Fatalf("should contain nothing") - } -} - -func TestLRU(t *testing.T) { - evictCounter := 0 - onEvicted := func(k int, v int) { - if k != v { - t.Fatalf("Evict values not equal (%v!=%v)", k, v) - } - evictCounter++ - } - l := NewLRU[int, int]( - WithCapacity[int, int](128), WithCallback[int, int](onEvicted)) - - for i := 0; i < 256; i++ { - l.Add(i, i) - } - if l.Len() != 128 { - t.Fatalf("bad len: %v", l.Len()) - } - - if evictCounter != 128 { - t.Fatalf("bad evict count: %v", evictCounter) - } - - for i, k := range l.Keys() { - if v, ok := l.Get(k); !ok || v != k || v != i+128 { - t.Fatalf("bad key: %v", k) - } - } - for i, v := range l.Values() { - if v != i+128 { - t.Fatalf("bad value: %v", v) - } - } - for i := 0; i < 128; i++ { - if _, ok := l.Get(i); ok { - t.Fatalf("should be evicted") - } - } - for i := 128; i < 256; i++ { - if _, ok := l.Get(i); !ok { - t.Fatalf("should not be evicted") - } - } - for i := 128; i < 192; i++ { - if ok := l.Remove(i); !ok { - t.Fatalf("should be contained") - } - if ok := l.Remove(i); ok { - t.Fatalf("should not be contained") - } - if _, ok := l.Get(i); ok { - t.Fatalf("should be deleted") - } - } - - l.Get(192) // expect 192 to be last key in l.Keys() - - for i, k := range l.Keys() { - if (i < 63 && k != i+193) || (i == 63 && k != 192) { - t.Fatalf("out of order key: %v", k) - } - } - - l.Purge() - if l.Len() != 0 { - t.Fatalf("bad len: %v", l.Len()) - } - if _, ok := l.Get(200); ok { - t.Fatalf("should contain nothing") - } -} - -func TestLRU_GetOldest_RemoveOldest(t *testing.T) { - l := NewLRU[int, int](WithCapacity[int, int](128)) - for i := 0; i < 256; i++ { - l.Add(i, i) - } - k, _, ok := l.GetOldest() - if !ok { - t.Fatalf("missing") - } - if k != 128 { - t.Fatalf("bad: %v", k) - } - - k, _, ok = l.RemoveOldest() - if !ok { - t.Fatalf("missing") - } - if k != 128 { - t.Fatalf("bad: %v", k) - } - - k, _, ok = l.RemoveOldest() - if !ok { - t.Fatalf("missing") - } - if k != 129 { - t.Fatalf("bad: %v", k) - } -} - -// Test that Add returns true/false if an eviction occurred -func TestLRU_Add(t *testing.T) { - evictCounter := 0 - onEvicted := func(k int, v int) { - evictCounter++ - } - - l := NewLRU[int, int]( - WithCapacity[int, int](1), WithCallback[int, int](onEvicted)) - - if l.Add(1, 1) == true || evictCounter != 0 { - t.Errorf("should not have an eviction") - } - if l.Add(2, 2) == false || evictCounter != 1 { - t.Errorf("should have an eviction") - } -} - -// Test that Contains doesn't update recent-ness -func TestLRU_Contains(t *testing.T) { - l := NewLRU[int, int](WithCapacity[int, int](2)) - - l.Add(1, 1) - l.Add(2, 2) - if !l.Contains(1) { - t.Errorf("1 should be contained") - } - - l.Add(3, 3) - if l.Contains(1) { - t.Errorf("Contains should not have updated recent-ness of 1") - } - - l.AddTTL(4, 4, time.Second) - if !l.Contains(4) { - t.Errorf("4 should be contained") - } - time.Sleep(time.Second) - if l.Contains(4) { - t.Errorf("Contains should not have updated recent-ness of 4") - } -} - -// Test that Peek doesn't update recent-ness -func TestLRU_Peek(t *testing.T) { - l := NewLRU[int, int](WithCapacity[int, int](2)) - - l.Add(1, 1) - l.Add(2, 2) - if v, ok := l.Peek(1); !ok || v != 1 { - t.Errorf("1 should be set to 1: %v, %v", v, ok) - } - - l.Add(3, 3) - if l.Contains(1) { - t.Errorf("should not have updated recent-ness of 1") - } - - l.AddTTL(4, 4, time.Second) - if v, ok := l.Peek(4); !ok || v != 4 { - t.Errorf("1 should be set to 1: %v, %v", v, ok) - } - time.Sleep(time.Second) - if l.Contains(4) { - t.Errorf("Contains should not have updated recent-ness of 4") - } -} - -// Test that Resize can upsize and downsize -func TestLRU_Resize(t *testing.T) { - onEvictCounter := 0 - onEvicted := func(k int, v int) { - onEvictCounter++ - } - l := NewLRU[int, int]( - WithCapacity[int, int](2), WithCallback[int, int](onEvicted)) - - // Downsize - l.Add(1, 1) - l.Add(2, 2) - evicted := l.Resize(1) - if evicted != 1 { - t.Errorf("1 element should have been evicted: %v", evicted) - } - if onEvictCounter != 1 { - t.Errorf("onEvicted should have been called 1 time: %v", onEvictCounter) - } - - l.Add(3, 3) - if l.Contains(1) { - t.Errorf("Element 1 should have been evicted") - } - - // Upsize - evicted = l.Resize(2) - if evicted != 0 { - t.Errorf("0 elements should have been evicted: %v", evicted) - } - - l.Add(4, 4) - if !l.Contains(3) || !l.Contains(4) { - t.Errorf("Cache should have contained 2 elements") - } -} - -func (l *LRU[K, V]) wantKeys(t *testing.T, want []K) { - t.Helper() - got := l.Keys() - if !reflect.DeepEqual(got, want) { - t.Errorf("wrong keys got: %v, want: %v ", got, want) - } -} - -func TestCache_EvictionSameKey(t *testing.T) { - var evictedKeys []int - onEvicted := func(key int, _ struct{}) { - evictedKeys = append(evictedKeys, key) - } - - l := NewLRU[int, struct{}]( - WithCapacity[int, struct{}](2), WithCallback[int, struct{}](onEvicted)) - - if evicted := l.Add(1, struct{}{}); evicted { - t.Error("First 1: got unexpected eviction") - } - l.wantKeys(t, []int{1}) - - if evicted := l.Add(2, struct{}{}); evicted { - t.Error("2: got unexpected eviction") - } - l.wantKeys(t, []int{1, 2}) - - if evicted := l.Add(1, struct{}{}); evicted { - t.Error("Second 1: got unexpected eviction") - } - l.wantKeys(t, []int{2, 1}) - - if evicted := l.Add(3, struct{}{}); !evicted { - t.Error("3: did not get expected eviction") - } - l.wantKeys(t, []int{1, 3}) - - want := []int{2} - if !reflect.DeepEqual(evictedKeys, want) { - t.Errorf("evictedKeys got: %v want: %v", evictedKeys, want) - } -} From f94e580d8a032bd45f4b2d946fe2601e759d71b5 Mon Sep 17 00:00:00 2001 From: Stone-afk <1711865140@qq.com> Date: Sun, 24 Dec 2023 23:47:44 +0800 Subject: [PATCH 06/16] feature add lru cache evict --- memory/lru/cache.go | 1 - 1 file changed, 1 deletion(-) diff --git a/memory/lru/cache.go b/memory/lru/cache.go index f785c07..a596e33 100644 --- a/memory/lru/cache.go +++ b/memory/lru/cache.go @@ -210,7 +210,6 @@ func (c *Cache) Set(ctx context.Context, key string, val any, expiration time.Du return nil } -// SetNX 由 strategy 统一控制过期时间 func (c *Cache) SetNX(ctx context.Context, key string, val any, expiration time.Duration) (bool, error) { c.lock.Lock() defer c.lock.Unlock() From 37abfc8beed9ee5e2b7535773faaa88c2374c8cc Mon Sep 17 00:00:00 2001 From: Stone-afk <1711865140@qq.com> Date: Mon, 25 Dec 2023 00:20:48 +0800 Subject: [PATCH 07/16] feature add lru cache evict --- memory/lru/list_test.go | 36 +++++++++++++----------------------- 1 file changed, 13 insertions(+), 23 deletions(-) diff --git a/memory/lru/list_test.go b/memory/lru/list_test.go index f9f0779..18e778d 100644 --- a/memory/lru/list_test.go +++ b/memory/lru/list_test.go @@ -20,14 +20,11 @@ import ( ) func Example() { - // Create a new list and put some numbers in it. l := newLinkedList[int]() e4 := l.PushBack(4) e1 := l.PushFront(1) l.InsertBefore(3, e4) l.InsertAfter(2, e1) - - // Iterate through list and print its contents. for e := l.Front(); e != nil; e = e.Next() { fmt.Println(e.Value) } @@ -54,16 +51,13 @@ func checkLinkedListPointers[T any](t *testing.T, l *linkedList[T], es []*elemen return } - // zero length LinkLists must be the zero value or properly initialized (sentinel circle) if len(es) == 0 { if l.root.next != nil && l.root.next != root || l.root.prev != nil && l.root.prev != root { t.Errorf("l.root.next = %p, l.root.prev = %p; both should both be nil or %p", l.root.next, l.root.prev, root) } return } - // len(es) > 0 - // check internal and external prev/next connections for i, e := range es { prev := root Prev := (*element[T])(nil) @@ -96,7 +90,6 @@ func checkLinkedListPointers[T any](t *testing.T, l *linkedList[T], es []*elemen func TestLinkedList(t *testing.T) { l := newLinkedList[any]() checkLinkedListPointers(t, l, []*element[any]{}) - // Single element LinkList e := l.PushFront("a") checkLinkedListPointers(t, l, []*element[any]{e}) l.MoveToFront(e) @@ -106,7 +99,6 @@ func TestLinkedList(t *testing.T) { l.Remove(e) checkLinkedListPointers(t, l, []*element[any]{}) - // Bigger LinkList e2 := l.PushFront(2) e1 := l.PushFront(1) e3 := l.PushBack(3) @@ -116,44 +108,43 @@ func TestLinkedList(t *testing.T) { l.Remove(e2) checkLinkedListPointers(t, l, []*element[any]{e1, e3, e4}) - l.MoveToFront(e3) // move from middle + l.MoveToFront(e3) checkLinkedListPointers(t, l, []*element[any]{e3, e1, e4}) l.MoveToFront(e1) - l.MoveToBack(e3) // move from middle + l.MoveToBack(e3) checkLinkedListPointers(t, l, []*element[any]{e1, e4, e3}) - l.MoveToFront(e3) // move from back + l.MoveToFront(e3) checkLinkedListPointers(t, l, []*element[any]{e3, e1, e4}) - l.MoveToFront(e3) // should be no-op + l.MoveToFront(e3) checkLinkedListPointers(t, l, []*element[any]{e3, e1, e4}) - l.MoveToBack(e3) // move from front + l.MoveToBack(e3) checkLinkedListPointers(t, l, []*element[any]{e1, e4, e3}) - l.MoveToBack(e3) // should be no-op + l.MoveToBack(e3) checkLinkedListPointers(t, l, []*element[any]{e1, e4, e3}) - e2 = l.InsertBefore(2, e1) // insert before front + e2 = l.InsertBefore(2, e1) checkLinkedListPointers(t, l, []*element[any]{e2, e1, e4, e3}) l.Remove(e2) - e2 = l.InsertBefore(2, e4) // insert before middle + e2 = l.InsertBefore(2, e4) checkLinkedListPointers(t, l, []*element[any]{e1, e2, e4, e3}) l.Remove(e2) - e2 = l.InsertBefore(2, e3) // insert before back + e2 = l.InsertBefore(2, e3) checkLinkedListPointers(t, l, []*element[any]{e1, e4, e2, e3}) l.Remove(e2) - e2 = l.InsertAfter(2, e1) // insert after front + e2 = l.InsertAfter(2, e1) checkLinkedListPointers(t, l, []*element[any]{e1, e2, e4, e3}) l.Remove(e2) - e2 = l.InsertAfter(2, e4) // insert after middle + e2 = l.InsertAfter(2, e4) checkLinkedListPointers(t, l, []*element[any]{e1, e4, e2, e3}) l.Remove(e2) - e2 = l.InsertAfter(2, e3) // insert after back + e2 = l.InsertAfter(2, e3) checkLinkedListPointers(t, l, []*element[any]{e1, e4, e3, e2}) l.Remove(e2) - // Check standard iteration. sum := 0 for e := l.Front(); e != nil; e = e.Next() { if i, ok := e.Value.(int); ok { @@ -164,7 +155,6 @@ func TestLinkedList(t *testing.T) { t.Errorf("sum over l = %d, want 4", sum) } - // Clear all elements by iterating var next *element[any] for e := l.Front(); e != nil; e = next { next = e.Next() @@ -255,7 +245,7 @@ func TestIssue4103Ele(t *testing.T) { l2.PushBack(4) e := l1.Front() - l2.Remove(e) // l2 should not change because e is not an element of l2 + l2.Remove(e) if n := l2.Len(); n != 2 { t.Errorf("l2.Len() = %d, want 2", n) } From 5ae3317902804520071d61de6bb663ad6bae2f2d Mon Sep 17 00:00:00 2001 From: Stone-afk <1711865140@qq.com> Date: Mon, 25 Dec 2023 17:04:09 +0800 Subject: [PATCH 08/16] feature add lru cache evict --- memory/lru/cache.go | 42 ++++------------------------------------ memory/lru/cache_test.go | 26 ++++++++++++------------- 2 files changed, 17 insertions(+), 51 deletions(-) diff --git a/memory/lru/cache.go b/memory/lru/cache.go index a596e33..e79f08d 100644 --- a/memory/lru/cache.go +++ b/memory/lru/cache.go @@ -33,24 +33,6 @@ var ( _ ecache.Cache = (*Cache)(nil) ) -//type entry[K comparable, V any] struct { -// key K -// value V -// expiresAt time.Time -//} -// -//func (e entry[K, V]) isExpired() bool { -// return e.expiresAt.Before(time.Now()) -//} -// -//func (e entry[K, V]) existExpiration() bool { -// return !e.expiresAt.IsZero() -//} - -const ( - defaultCapacity = 100 -) - type entry struct { key string value any @@ -69,18 +51,12 @@ type EvictCallback func(key string, value any) type Option func(l *Cache) -func WithCallback(callback func(k string, v any)) Option { +func WithEvictCallback(callback func(k string, v any)) Option { return func(l *Cache) { l.callback = callback } } -func WithCapacity(capacity int) Option { - return func(l *Cache) { - l.capacity = capacity - } -} - type Cache struct { lock sync.RWMutex capacity int @@ -89,11 +65,11 @@ type Cache struct { callback EvictCallback } -func NewCache(options ...Option) *Cache { +func NewCache(capacity int, options ...Option) *Cache { res := &Cache{ list: newLinkedList[entry](), - data: make(map[string]*element[entry], 16), - capacity: defaultCapacity, + data: make(map[string]*element[entry], capacity), + capacity: capacity, } for _, opt := range options { opt(res) @@ -140,16 +116,6 @@ func (c *Cache) get(key string) (value any, ok bool) { return } -func (c *Cache) RemoveOldest() (key string, value any, ok bool) { - c.lock.Lock() - defer c.lock.Unlock() - if elem := c.list.Back(); elem != nil { - c.removeElement(elem) - return elem.Value.key, elem.Value.value, true - } - return -} - func (c *Cache) removeOldest() { if elem := c.list.Back(); elem != nil { c.removeElement(elem) diff --git a/memory/lru/cache_test.go b/memory/lru/cache_test.go index 42cdfab..02ec631 100644 --- a/memory/lru/cache_test.go +++ b/memory/lru/cache_test.go @@ -34,7 +34,7 @@ func TestCache_Set(t *testing.T) { onEvicted := func(key string, value any) { evictCounter++ } - cache := NewCache(WithCapacity(5), WithCallback(onEvicted)) + cache := NewCache(5, WithEvictCallback(onEvicted)) testCase := []struct { name string @@ -77,7 +77,7 @@ func TestCache_Get(t *testing.T) { onEvicted := func(key string, value any) { evictCounter++ } - cache := NewCache(WithCapacity(5), WithCallback(onEvicted)) + cache := NewCache(5, WithEvictCallback(onEvicted)) testCase := []struct { name string @@ -101,7 +101,7 @@ func TestCache_Get(t *testing.T) { wantVal: "hello ecache", }, { - name: "get expired value", + name: "get set TTL value", before: func(t *testing.T) { assert.Equal(t, false, cache.addTTL("test", "hello ecache", time.Second)) @@ -143,7 +143,7 @@ func TestCache_SetNX(t *testing.T) { onEvicted := func(key string, value any) { evictCounter++ } - cache := NewCache(WithCapacity(1), WithCallback(onEvicted)) + cache := NewCache(1, WithEvictCallback(onEvicted)) testCase := []struct { name string @@ -212,7 +212,7 @@ func TestCache_GetSet(t *testing.T) { onEvicted := func(key string, value any) { evictCounter++ } - cache := NewCache(WithCapacity(5), WithCallback(onEvicted)) + cache := NewCache(5, WithEvictCallback(onEvicted)) testCase := []struct { name string @@ -270,7 +270,7 @@ func TestCache_GetSet(t *testing.T) { } func TestCache_Delete(t *testing.T) { - cache := NewCache(WithCapacity(5)) + cache := NewCache(5) testCases := []struct { name string @@ -404,7 +404,7 @@ func TestCache_LPush(t *testing.T) { onEvicted := func(key string, value any) { evictCounter++ } - cache := NewCache(WithCapacity(5), WithCallback(onEvicted)) + cache := NewCache(5, WithEvictCallback(onEvicted)) testCase := []struct { name string @@ -486,7 +486,7 @@ func TestCache_LPop(t *testing.T) { onEvicted := func(key string, value any) { evictCounter++ } - cache := NewCache(WithCapacity(5), WithCallback(onEvicted)) + cache := NewCache(5, WithEvictCallback(onEvicted)) testCase := []struct { name string @@ -581,7 +581,7 @@ func TestCache_SAdd(t *testing.T) { onEvicted := func(key string, value any) { evictCounter++ } - cache := NewCache(WithCapacity(5), WithCallback(onEvicted)) + cache := NewCache(5, WithEvictCallback(onEvicted)) testCase := []struct { name string @@ -651,7 +651,7 @@ func TestCache_SRem(t *testing.T) { onEvicted := func(key string, value any) { evictCounter++ } - cache := NewCache(WithCapacity(5), WithCallback(onEvicted)) + cache := NewCache(5, WithEvictCallback(onEvicted)) testCase := []struct { name string @@ -737,7 +737,7 @@ func TestCache_IncrBy(t *testing.T) { onEvicted := func(key string, value any) { evictCounter++ } - cache := NewCache(WithCapacity(5), WithCallback(onEvicted)) + cache := NewCache(5, WithEvictCallback(onEvicted)) testCase := []struct { name string @@ -804,7 +804,7 @@ func TestCache_DecrBy(t *testing.T) { onEvicted := func(key string, value any) { evictCounter++ } - cache := NewCache(WithCapacity(5), WithCallback(onEvicted)) + cache := NewCache(5, WithEvictCallback(onEvicted)) testCase := []struct { name string @@ -871,7 +871,7 @@ func TestCache_IncrByFloat(t *testing.T) { onEvicted := func(key string, value any) { evictCounter++ } - cache := NewCache(WithCapacity(5), WithCallback(onEvicted)) + cache := NewCache(5, WithEvictCallback(onEvicted)) testCase := []struct { name string From 056c4c039c7a7bbf59920e6977a2898e691bd298 Mon Sep 17 00:00:00 2001 From: Stone-afk <1711865140@qq.com> Date: Tue, 26 Dec 2023 10:32:50 +0800 Subject: [PATCH 09/16] feature add lru cache evict --- memory/lru/cache.go | 8 +- memory/lru/list.go | 54 ++++++------ memory/lru/list_test.go | 188 ++++++++++++++++++++-------------------- 3 files changed, 125 insertions(+), 125 deletions(-) diff --git a/memory/lru/cache.go b/memory/lru/cache.go index e79f08d..2db74c2 100644 --- a/memory/lru/cache.go +++ b/memory/lru/cache.go @@ -80,10 +80,10 @@ func NewCache(capacity int, options ...Option) *Cache { func (c *Cache) pushEntry(key string, ent entry) (evicted bool) { if elem, ok := c.data[key]; ok { elem.Value = ent - c.list.MoveToFront(elem) + c.list.moveToFront(elem) return false } - elem := c.list.PushFront(ent) + elem := c.list.pushFront(ent) c.data[key] = elem evict := c.len() > c.capacity if evict { @@ -110,7 +110,7 @@ func (c *Cache) get(key string) (value any, ok bool) { c.removeElement(elem) return } - c.list.MoveToFront(elem) + c.list.moveToFront(elem) return ent.value, true } return @@ -159,7 +159,7 @@ func (c *Cache) delete(key string) { func (c *Cache) len() int { var length int - for elem := c.list.Back(); elem != nil; elem = elem.Prev() { + for elem := c.list.Back(); elem != nil; elem = elem.prevElem() { if elem.Value.existExpiration() && elem.Value.isExpired() { c.removeElement(elem) continue diff --git a/memory/lru/list.go b/memory/lru/list.go index 5a326e8..9b38ae6 100644 --- a/memory/lru/list.go +++ b/memory/lru/list.go @@ -20,14 +20,14 @@ type element[T any] struct { next, prev *element[T] } -func (e *element[T]) Next() *element[T] { +func (e *element[T]) nextElem() *element[T] { if n := e.next; e.list != nil && n != &e.list.root { return n } return nil } -func (e *element[T]) Prev() *element[T] { +func (e *element[T]) prevElem() *element[T] { if p := e.prev; e.list != nil && p != &e.list.root { return p } @@ -35,35 +35,35 @@ func (e *element[T]) Prev() *element[T] { } type linkedList[T any] struct { - root element[T] - len int + root element[T] + capacity int } func newLinkedList[T any]() *linkedList[T] { l := &linkedList[T]{} - return l.Init() + return l.init() } -func (l *linkedList[T]) Init() *linkedList[T] { +func (l *linkedList[T]) init() *linkedList[T] { l.root.next = &l.root l.root.prev = &l.root - l.len = 0 + l.capacity = 0 return l } -func (l *linkedList[T]) Len() int { - return l.len +func (l *linkedList[T]) len() int { + return l.capacity } func (l *linkedList[T]) Front() *element[T] { - if l.len == 0 { + if l.capacity == 0 { return nil } return l.root.next } func (l *linkedList[T]) Back() *element[T] { - if l.len == 0 { + if l.capacity == 0 { return nil } return l.root.prev @@ -71,7 +71,7 @@ func (l *linkedList[T]) Back() *element[T] { func (l *linkedList[T]) lazyInit() { if l.root.next == nil { - l.Init() + l.init() } } @@ -81,7 +81,7 @@ func (l *linkedList[T]) insert(e, at *element[T]) *element[T] { e.prev.next = e e.next.prev = e e.list = l - l.len++ + l.capacity++ return e } @@ -95,7 +95,7 @@ func (l *linkedList[T]) remove(e *element[T]) { e.next = nil e.prev = nil e.list = nil - l.len-- + l.capacity-- } func (l *linkedList[T]) move(e, at *element[T]) { @@ -118,70 +118,70 @@ func (l *linkedList[T]) Remove(e *element[T]) any { return e.Value } -func (l *linkedList[T]) PushFront(v T) *element[T] { +func (l *linkedList[T]) pushFront(v T) *element[T] { l.lazyInit() return l.insertValue(v, &l.root) } -func (l *linkedList[T]) PushBack(v T) *element[T] { +func (l *linkedList[T]) pushBack(v T) *element[T] { l.lazyInit() return l.insertValue(v, l.root.prev) } -func (l *linkedList[T]) MoveToFront(e *element[T]) { +func (l *linkedList[T]) moveToFront(e *element[T]) { if e.list != l || l.root.next == e { return } l.move(e, &l.root) } -func (l *linkedList[T]) MoveToBack(e *element[T]) { +func (l *linkedList[T]) moveToBack(e *element[T]) { if e.list != l || l.root.prev == e { return } l.move(e, l.root.prev) } -func (l *linkedList[T]) MoveBefore(e, mark *element[T]) { +func (l *linkedList[T]) moveBefore(e, mark *element[T]) { if e.list != l || e == mark || mark.list != l { return } l.move(e, mark.prev) } -func (l *linkedList[T]) MoveAfter(e, mark *element[T]) { +func (l *linkedList[T]) moveAfter(e, mark *element[T]) { if e.list != l || e == mark || mark.list != l { return } l.move(e, mark) } -func (l *linkedList[T]) InsertBefore(v T, mark *element[T]) *element[T] { +func (l *linkedList[T]) insertBefore(v T, mark *element[T]) *element[T] { if mark.list != l { return nil } return l.insertValue(v, mark.prev) } -func (l *linkedList[T]) InsertAfter(v T, mark *element[T]) *element[T] { +func (l *linkedList[T]) insertAfter(v T, mark *element[T]) *element[T] { if mark.list != l { return nil } return l.insertValue(v, mark) } -func (l *linkedList[T]) PushBackList(other *linkedList[T]) { +func (l *linkedList[T]) pushBackList(other *linkedList[T]) { l.lazyInit() e := other.Front() - for i := other.Len(); i > 0; i-- { + for i := other.len(); i > 0; i-- { l.insertValue(e.Value, l.root.prev) - e = e.Next() + e = e.nextElem() } } -func (l *linkedList[T]) PushFrontList(other *linkedList[T]) { +func (l *linkedList[T]) pushFrontList(other *linkedList[T]) { l.lazyInit() - for i, e := other.Len(), other.Back(); i > 0; i, e = i-1, e.Prev() { + for i, e := other.len(), other.Back(); i > 0; i, e = i-1, e.prevElem() { l.insertValue(e.Value, &l.root) } } diff --git a/memory/lru/list_test.go b/memory/lru/list_test.go index 18e778d..8793c7d 100644 --- a/memory/lru/list_test.go +++ b/memory/lru/list_test.go @@ -21,11 +21,11 @@ import ( func Example() { l := newLinkedList[int]() - e4 := l.PushBack(4) - e1 := l.PushFront(1) - l.InsertBefore(3, e4) - l.InsertAfter(2, e1) - for e := l.Front(); e != nil; e = e.Next() { + e4 := l.pushBack(4) + e1 := l.pushFront(1) + l.insertBefore(3, e4) + l.insertAfter(2, e1) + for e := l.Front(); e != nil; e = e.nextElem() { fmt.Println(e.Value) } @@ -37,8 +37,8 @@ func Example() { } func checkLinkedListLen[T any](t *testing.T, l *linkedList[T], len int) bool { - if n := l.Len(); n != len { - t.Errorf("l.Len() = %d, want %d", n, len) + if n := l.len(); n != len { + t.Errorf("l.len() = %d, want %d", n, len) return false } return true @@ -68,8 +68,8 @@ func checkLinkedListPointers[T any](t *testing.T, l *linkedList[T], es []*elemen if p := e.prev; p != prev { t.Errorf("elt[%d](%p).prev = %p, want %p", i, e, p, prev) } - if p := e.Prev(); p != Prev { - t.Errorf("elt[%d](%p).Prev() = %p, want %p", i, e, p, Prev) + if p := e.prevElem(); p != Prev { + t.Errorf("elt[%d](%p).prevElem() = %p, want %p", i, e, p, Prev) } next := root @@ -81,8 +81,8 @@ func checkLinkedListPointers[T any](t *testing.T, l *linkedList[T], es []*elemen if n := e.next; n != next { t.Errorf("elt[%d](%p).next = %p, want %p", i, e, n, next) } - if n := e.Next(); n != Next { - t.Errorf("elt[%d](%p).Next() = %p, want %p", i, e, n, Next) + if n := e.nextElem(); n != Next { + t.Errorf("elt[%d](%p).nextElem() = %p, want %p", i, e, n, Next) } } } @@ -90,63 +90,63 @@ func checkLinkedListPointers[T any](t *testing.T, l *linkedList[T], es []*elemen func TestLinkedList(t *testing.T) { l := newLinkedList[any]() checkLinkedListPointers(t, l, []*element[any]{}) - e := l.PushFront("a") + e := l.pushFront("a") checkLinkedListPointers(t, l, []*element[any]{e}) - l.MoveToFront(e) + l.moveToFront(e) checkLinkedListPointers(t, l, []*element[any]{e}) - l.MoveToBack(e) + l.moveToBack(e) checkLinkedListPointers(t, l, []*element[any]{e}) l.Remove(e) checkLinkedListPointers(t, l, []*element[any]{}) - e2 := l.PushFront(2) - e1 := l.PushFront(1) - e3 := l.PushBack(3) - e4 := l.PushBack("banana") + e2 := l.pushFront(2) + e1 := l.pushFront(1) + e3 := l.pushBack(3) + e4 := l.pushBack("banana") checkLinkedListPointers(t, l, []*element[any]{e1, e2, e3, e4}) l.Remove(e2) checkLinkedListPointers(t, l, []*element[any]{e1, e3, e4}) - l.MoveToFront(e3) + l.moveToFront(e3) checkLinkedListPointers(t, l, []*element[any]{e3, e1, e4}) - l.MoveToFront(e1) - l.MoveToBack(e3) + l.moveToFront(e1) + l.moveToBack(e3) checkLinkedListPointers(t, l, []*element[any]{e1, e4, e3}) - l.MoveToFront(e3) + l.moveToFront(e3) checkLinkedListPointers(t, l, []*element[any]{e3, e1, e4}) - l.MoveToFront(e3) + l.moveToFront(e3) checkLinkedListPointers(t, l, []*element[any]{e3, e1, e4}) - l.MoveToBack(e3) + l.moveToBack(e3) checkLinkedListPointers(t, l, []*element[any]{e1, e4, e3}) - l.MoveToBack(e3) + l.moveToBack(e3) checkLinkedListPointers(t, l, []*element[any]{e1, e4, e3}) - e2 = l.InsertBefore(2, e1) + e2 = l.insertBefore(2, e1) checkLinkedListPointers(t, l, []*element[any]{e2, e1, e4, e3}) l.Remove(e2) - e2 = l.InsertBefore(2, e4) + e2 = l.insertBefore(2, e4) checkLinkedListPointers(t, l, []*element[any]{e1, e2, e4, e3}) l.Remove(e2) - e2 = l.InsertBefore(2, e3) + e2 = l.insertBefore(2, e3) checkLinkedListPointers(t, l, []*element[any]{e1, e4, e2, e3}) l.Remove(e2) - e2 = l.InsertAfter(2, e1) + e2 = l.insertAfter(2, e1) checkLinkedListPointers(t, l, []*element[any]{e1, e2, e4, e3}) l.Remove(e2) - e2 = l.InsertAfter(2, e4) + e2 = l.insertAfter(2, e4) checkLinkedListPointers(t, l, []*element[any]{e1, e4, e2, e3}) l.Remove(e2) - e2 = l.InsertAfter(2, e3) + e2 = l.insertAfter(2, e3) checkLinkedListPointers(t, l, []*element[any]{e1, e4, e3, e2}) l.Remove(e2) sum := 0 - for e := l.Front(); e != nil; e = e.Next() { + for e := l.Front(); e != nil; e = e.nextElem() { if i, ok := e.Value.(int); ok { sum += i } @@ -157,7 +157,7 @@ func TestLinkedList(t *testing.T) { var next *element[any] for e := l.Front(); e != nil; e = next { - next = e.Next() + next = e.nextElem() l.Remove(e) } checkLinkedListPointers(t, l, []*element[any]{}) @@ -169,7 +169,7 @@ func checkLinkedList[T int](t *testing.T, l *linkedList[T], es []any) { } i := 0 - for e := l.Front(); e != nil; e = e.Next() { + for e := l.Front(); e != nil; e = e.nextElem() { le := e.Value if le != es[i] { t.Errorf("elt[%d].Value = %v, want %v", i, le, es[i]) @@ -182,51 +182,51 @@ func TestExtendingEle(t *testing.T) { l1 := newLinkedList[int]() l2 := newLinkedList[int]() - l1.PushBack(1) - l1.PushBack(2) - l1.PushBack(3) + l1.pushBack(1) + l1.pushBack(2) + l1.pushBack(3) - l2.PushBack(4) - l2.PushBack(5) + l2.pushBack(4) + l2.pushBack(5) l3 := newLinkedList[int]() - l3.PushBackList(l1) + l3.pushBackList(l1) checkLinkedList(t, l3, []any{1, 2, 3}) - l3.PushBackList(l2) + l3.pushBackList(l2) checkLinkedList(t, l3, []any{1, 2, 3, 4, 5}) l3 = newLinkedList[int]() - l3.PushFrontList(l2) + l3.pushFrontList(l2) checkLinkedList(t, l3, []any{4, 5}) - l3.PushFrontList(l1) + l3.pushFrontList(l1) checkLinkedList(t, l3, []any{1, 2, 3, 4, 5}) checkLinkedList(t, l1, []any{1, 2, 3}) checkLinkedList(t, l2, []any{4, 5}) l3 = newLinkedList[int]() - l3.PushBackList(l1) + l3.pushBackList(l1) checkLinkedList(t, l3, []any{1, 2, 3}) - l3.PushBackList(l3) + l3.pushBackList(l3) checkLinkedList(t, l3, []any{1, 2, 3, 1, 2, 3}) l3 = newLinkedList[int]() - l3.PushFrontList(l1) + l3.pushFrontList(l1) checkLinkedList(t, l3, []any{1, 2, 3}) - l3.PushFrontList(l3) + l3.pushFrontList(l3) checkLinkedList(t, l3, []any{1, 2, 3, 1, 2, 3}) l3 = newLinkedList[int]() - l1.PushBackList(l3) + l1.pushBackList(l3) checkLinkedList(t, l1, []any{1, 2, 3}) - l1.PushFrontList(l3) + l1.pushFrontList(l3) checkLinkedList(t, l1, []any{1, 2, 3}) } func TestRemoveEle(t *testing.T) { l := newLinkedList[int]() - e1 := l.PushBack(1) - e2 := l.PushBack(2) + e1 := l.pushBack(1) + e2 := l.pushBack(2) checkLinkedListPointers(t, l, []*element[int]{e1, e2}) e := l.Front() l.Remove(e) @@ -237,124 +237,124 @@ func TestRemoveEle(t *testing.T) { func TestIssue4103Ele(t *testing.T) { l1 := newLinkedList[int]() - l1.PushBack(1) - l1.PushBack(2) + l1.pushBack(1) + l1.pushBack(2) l2 := newLinkedList[int]() - l2.PushBack(3) - l2.PushBack(4) + l2.pushBack(3) + l2.pushBack(4) e := l1.Front() l2.Remove(e) - if n := l2.Len(); n != 2 { - t.Errorf("l2.Len() = %d, want 2", n) + if n := l2.len(); n != 2 { + t.Errorf("l2.len() = %d, want 2", n) } - l1.InsertBefore(8, e) - if n := l1.Len(); n != 3 { - t.Errorf("l1.Len() = %d, want 3", n) + l1.insertBefore(8, e) + if n := l1.len(); n != 3 { + t.Errorf("l1.len() = %d, want 3", n) } } func TestIssue6349Ele(t *testing.T) { l := newLinkedList[int]() - l.PushBack(1) - l.PushBack(2) + l.pushBack(1) + l.pushBack(2) e := l.Front() l.Remove(e) if e.Value != 1 { t.Errorf("e.value = %d, want 1", e.Value) } - if e.Next() != nil { - t.Errorf("e.Next() != nil") + if e.nextElem() != nil { + t.Errorf("e.nextElem() != nil") } - if e.Prev() != nil { - t.Errorf("e.Prev() != nil") + if e.prevElem() != nil { + t.Errorf("e.prevElem() != nil") } } func TestMoveEle(t *testing.T) { l := newLinkedList[int]() - e1 := l.PushBack(1) - e2 := l.PushBack(2) - e3 := l.PushBack(3) - e4 := l.PushBack(4) + e1 := l.pushBack(1) + e2 := l.pushBack(2) + e3 := l.pushBack(3) + e4 := l.pushBack(4) - l.MoveAfter(e3, e3) + l.moveAfter(e3, e3) checkLinkedListPointers(t, l, []*element[int]{e1, e2, e3, e4}) - l.MoveBefore(e2, e2) + l.moveBefore(e2, e2) checkLinkedListPointers(t, l, []*element[int]{e1, e2, e3, e4}) - l.MoveAfter(e3, e2) + l.moveAfter(e3, e2) checkLinkedListPointers(t, l, []*element[int]{e1, e2, e3, e4}) - l.MoveBefore(e2, e3) + l.moveBefore(e2, e3) checkLinkedListPointers(t, l, []*element[int]{e1, e2, e3, e4}) - l.MoveBefore(e2, e4) + l.moveBefore(e2, e4) checkLinkedListPointers(t, l, []*element[int]{e1, e3, e2, e4}) e2, e3 = e3, e2 - l.MoveBefore(e4, e1) + l.moveBefore(e4, e1) checkLinkedListPointers(t, l, []*element[int]{e4, e1, e2, e3}) e1, e2, e3, e4 = e4, e1, e2, e3 - l.MoveAfter(e4, e1) + l.moveAfter(e4, e1) checkLinkedListPointers(t, l, []*element[int]{e1, e4, e2, e3}) e2, e3, e4 = e4, e2, e3 - l.MoveAfter(e2, e3) + l.moveAfter(e2, e3) checkLinkedListPointers(t, l, []*element[int]{e1, e3, e2, e4}) } func TestZeroLinkedList(t *testing.T) { var l1 = new(linkedList[int]) - l1.PushFront(1) + l1.pushFront(1) checkLinkedList(t, l1, []any{1}) var l2 = new(linkedList[int]) - l2.PushBack(1) + l2.pushBack(1) checkLinkedList(t, l2, []any{1}) var l3 = new(linkedList[int]) - l3.PushFrontList(l1) + l3.pushFrontList(l1) checkLinkedList(t, l3, []any{1}) var l4 = new(linkedList[int]) - l4.PushBackList(l2) + l4.pushBackList(l2) checkLinkedList(t, l4, []any{1}) } func TestInsertBeforeUnknownMarkEle(t *testing.T) { var l linkedList[int] - l.PushBack(1) - l.PushBack(2) - l.PushBack(3) - l.InsertBefore(1, new(element[int])) + l.pushBack(1) + l.pushBack(2) + l.pushBack(3) + l.insertBefore(1, new(element[int])) checkLinkedList(t, &l, []any{1, 2, 3}) } func TestInsertAfterUnknownMarkEle(t *testing.T) { var l linkedList[int] - l.PushBack(1) - l.PushBack(2) - l.PushBack(3) - l.InsertAfter(1, new(element[int])) + l.pushBack(1) + l.pushBack(2) + l.pushBack(3) + l.insertAfter(1, new(element[int])) checkLinkedList(t, &l, []any{1, 2, 3}) } func TestMoveUnknownMarkEle(t *testing.T) { var l1 linkedList[int] - e1 := l1.PushBack(1) + e1 := l1.pushBack(1) var l2 linkedList[int] - e2 := l2.PushBack(2) + e2 := l2.pushBack(2) - l1.MoveAfter(e1, e2) + l1.moveAfter(e1, e2) checkLinkedList(t, &l1, []any{1}) checkLinkedList(t, &l2, []any{2}) - l1.MoveBefore(e1, e2) + l1.moveBefore(e1, e2) checkLinkedList(t, &l1, []any{1}) checkLinkedList(t, &l2, []any{2}) } From fb71f6641b2f484932c2c95b3bd9215e214ed5b7 Mon Sep 17 00:00:00 2001 From: Stone-afk <1711865140@qq.com> Date: Wed, 27 Dec 2023 14:28:33 +0800 Subject: [PATCH 10/16] feature: add lru --- memory/lru/cache.go | 35 ++++++++++++++------------------ memory/lru/cache_test.go | 44 ++++++++++++++++++++++------------------ memory/lru/list.go | 10 ++++----- memory/lru/list_test.go | 40 ++++++++++++++++++------------------ 4 files changed, 64 insertions(+), 65 deletions(-) diff --git a/memory/lru/cache.go b/memory/lru/cache.go index 2db74c2..1caaa51 100644 --- a/memory/lru/cache.go +++ b/memory/lru/cache.go @@ -40,11 +40,7 @@ type entry struct { } func (e entry) isExpired() bool { - return e.expiresAt.Before(time.Now()) -} - -func (e entry) existExpiration() bool { - return !e.expiresAt.IsZero() + return !e.expiresAt.IsZero() && e.expiresAt.Before(time.Now()) } type EvictCallback func(key string, value any) @@ -77,7 +73,10 @@ func NewCache(capacity int, options ...Option) *Cache { return res } -func (c *Cache) pushEntry(key string, ent entry) (evicted bool) { +func (c *Cache) pushEntry(key string, ent entry) bool { + if c.len() > c.capacity { + c.removeOldest() + } if elem, ok := c.data[key]; ok { elem.Value = ent c.list.moveToFront(elem) @@ -85,20 +84,16 @@ func (c *Cache) pushEntry(key string, ent entry) (evicted bool) { } elem := c.list.pushFront(ent) c.data[key] = elem - evict := c.len() > c.capacity - if evict { - c.removeOldest() - } - return evict + return true } -func (c *Cache) addTTL(key string, value any, expiration time.Duration) (evicted bool) { +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) (evicted bool) { +func (c *Cache) add(key string, value any) bool { ent := entry{key: key, value: value} return c.pushEntry(key, ent) } @@ -106,7 +101,7 @@ func (c *Cache) add(key string, value any) (evicted bool) { func (c *Cache) get(key string) (value any, ok bool) { if elem, exist := c.data[key]; exist { ent := elem.Value - if ent.existExpiration() && ent.isExpired() { + if ent.isExpired() { c.removeElement(elem) return } @@ -117,13 +112,13 @@ func (c *Cache) get(key string) (value any, ok bool) { } func (c *Cache) removeOldest() { - if elem := c.list.Back(); elem != nil { + if elem := c.list.back(); elem != nil { c.removeElement(elem) } } func (c *Cache) removeElement(elem *element[entry]) { - c.list.Remove(elem) + c.list.removeElem(elem) ent := elem.Value c.delete(ent.key) if c.callback != nil { @@ -134,7 +129,7 @@ func (c *Cache) removeElement(elem *element[entry]) { func (c *Cache) remove(key string) (present bool) { if elem, ok := c.data[key]; ok { c.removeElement(elem) - if elem.Value.existExpiration() && elem.Value.isExpired() { + if elem.Value.isExpired() { return false } return true @@ -145,7 +140,7 @@ func (c *Cache) remove(key string) (present bool) { func (c *Cache) contains(key string) (ok bool) { elem, ok := c.data[key] if ok { - if elem.Value.existExpiration() && elem.Value.isExpired() { + if elem.Value.isExpired() { c.removeElement(elem) return false } @@ -159,8 +154,8 @@ func (c *Cache) delete(key string) { func (c *Cache) len() int { var length int - for elem := c.list.Back(); elem != nil; elem = elem.prevElem() { - if elem.Value.existExpiration() && elem.Value.isExpired() { + for elem := c.list.back(); elem != nil; elem = elem.prevElem() { + if elem.Value.isExpired() { c.removeElement(elem) continue } diff --git a/memory/lru/cache_test.go b/memory/lru/cache_test.go index 02ec631..dfb4b08 100644 --- a/memory/lru/cache_test.go +++ b/memory/lru/cache_test.go @@ -92,10 +92,12 @@ func TestCache_Get(t *testing.T) { { name: "get value", before: func(t *testing.T) { - assert.Equal(t, false, cache.add("test", "hello ecache")) + assert.Equal(t, true, cache.add("test", "hello ecache")) + assert.Equal(t, 0, evictCounter) }, after: func(t *testing.T) { assert.Equal(t, true, cache.remove("test")) + assert.Equal(t, 1, evictCounter) }, key: "test", wantVal: "hello ecache", @@ -103,13 +105,15 @@ func TestCache_Get(t *testing.T) { { name: "get set TTL value", before: func(t *testing.T) { - assert.Equal(t, false, + assert.Equal(t, true, cache.addTTL("test", "hello ecache", time.Second)) + assert.Equal(t, 1, evictCounter) }, after: func(t *testing.T) { time.Sleep(time.Second) _, ok := cache.get("test") assert.Equal(t, false, ok) + assert.Equal(t, 2, evictCounter) }, key: "test", wantVal: "hello ecache", @@ -180,7 +184,7 @@ func TestCache_SetNX(t *testing.T) { { name: "setnx expired value", before: func(t *testing.T) { - assert.Equal(t, false, cache.addTTL("test", "hello ecache", time.Second)) + assert.Equal(t, true, cache.addTTL("test", "hello ecache", time.Second)) }, after: func(t *testing.T) { time.Sleep(time.Second) @@ -227,7 +231,7 @@ func TestCache_GetSet(t *testing.T) { { name: "getset value", before: func(t *testing.T) { - assert.Equal(t, false, cache.add("test", "hello ecache")) + assert.Equal(t, true, cache.add("test", "hello ecache")) }, after: func(t *testing.T) { result, ok := cache.get("test") @@ -444,7 +448,7 @@ func TestCache_LPush(t *testing.T) { l := &list.ConcurrentList[ecache.Value]{ List: list.NewLinkedListOf[ecache.Value]([]ecache.Value{val}), } - assert.Equal(t, false, cache.add("test", l)) + assert.Equal(t, true, cache.add("test", l)) }, after: func(t *testing.T) { assert.Equal(t, true, cache.remove("test")) @@ -456,7 +460,7 @@ func TestCache_LPush(t *testing.T) { { name: "lpush value not type", before: func(t *testing.T) { - assert.Equal(t, false, cache.add("test", "string")) + assert.Equal(t, true, cache.add("test", "string")) }, after: func(t *testing.T) { assert.Equal(t, true, cache.remove("test")) @@ -505,7 +509,7 @@ func TestCache_LPop(t *testing.T) { l := &list.ConcurrentList[ecache.Value]{ List: list.NewLinkedListOf[ecache.Value]([]ecache.Value{val}), } - assert.Equal(t, false, cache.add("test", l)) + assert.Equal(t, true, cache.add("test", l)) }, after: func(t *testing.T) { assert.Equal(t, true, cache.remove("test")) @@ -523,7 +527,7 @@ func TestCache_LPop(t *testing.T) { l := &list.ConcurrentList[ecache.Value]{ List: list.NewLinkedListOf[ecache.Value]([]ecache.Value{val, val2}), } - assert.Equal(t, false, cache.add("test", l)) + assert.Equal(t, true, cache.add("test", l)) }, after: func(t *testing.T) { val, ok := cache.get("test") @@ -544,7 +548,7 @@ func TestCache_LPop(t *testing.T) { { name: "lpop value type error", before: func(t *testing.T) { - assert.Equal(t, false, cache.add("test", "hello world")) + assert.Equal(t, true, cache.add("test", "hello world")) }, after: func(t *testing.T) { assert.Equal(t, true, cache.remove("test")) @@ -609,7 +613,7 @@ func TestCache_SAdd(t *testing.T) { s := set.NewMapSet[any](8) s.Add("hello world") - assert.Equal(t, false, cache.add("test", s)) + assert.Equal(t, true, cache.add("test", s)) }, after: func(t *testing.T) { assert.Equal(t, true, cache.remove("test")) @@ -621,7 +625,7 @@ func TestCache_SAdd(t *testing.T) { { name: "sadd value type err", before: func(t *testing.T) { - assert.Equal(t, false, cache.add("test", "string")) + assert.Equal(t, true, cache.add("test", "string")) }, after: func(t *testing.T) { assert.Equal(t, true, cache.remove("test")) @@ -672,7 +676,7 @@ func TestCache_SRem(t *testing.T) { s.Add("hello world") s.Add("hello ecache") - assert.Equal(t, false, cache.add("test", s)) + assert.Equal(t, true, cache.add("test", s)) }, after: func(t *testing.T) { assert.Equal(t, true, cache.remove("test")) @@ -687,7 +691,7 @@ func TestCache_SRem(t *testing.T) { s := set.NewMapSet[any](8) s.Add("hello world") - assert.Equal(t, false, cache.add("test", s)) + assert.Equal(t, true, cache.add("test", s)) }, after: func(t *testing.T) { assert.Equal(t, true, cache.remove("test")) @@ -707,7 +711,7 @@ func TestCache_SRem(t *testing.T) { { name: "srem value type error", before: func(t *testing.T) { - assert.Equal(t, false, cache.add("test", int64(1))) + assert.Equal(t, true, cache.add("test", int64(1))) }, after: func(t *testing.T) { assert.Equal(t, true, cache.remove("test")) @@ -762,7 +766,7 @@ func TestCache_IncrBy(t *testing.T) { { name: "incrby value add", before: func(t *testing.T) { - assert.Equal(t, false, cache.add("test", int64(1))) + assert.Equal(t, true, cache.add("test", int64(1))) }, after: func(t *testing.T) { assert.Equal(t, true, cache.remove("test")) @@ -774,7 +778,7 @@ func TestCache_IncrBy(t *testing.T) { { name: "incrby value type error", before: func(t *testing.T) { - assert.Equal(t, false, cache.add("test", 12.62)) + assert.Equal(t, true, cache.add("test", 12.62)) }, after: func(t *testing.T) { assert.Equal(t, true, cache.remove("test")) @@ -829,7 +833,7 @@ func TestCache_DecrBy(t *testing.T) { { name: "decrby old value", before: func(t *testing.T) { - assert.Equal(t, false, cache.add("test", int64(3))) + assert.Equal(t, true, cache.add("test", int64(3))) }, after: func(t *testing.T) { assert.Equal(t, true, cache.remove("test")) @@ -841,7 +845,7 @@ func TestCache_DecrBy(t *testing.T) { { name: "decrby value type error", before: func(t *testing.T) { - assert.Equal(t, false, cache.add("test", 3.156)) + assert.Equal(t, true, cache.add("test", 3.156)) }, after: func(t *testing.T) { assert.Equal(t, true, cache.remove("test")) @@ -896,7 +900,7 @@ func TestCache_IncrByFloat(t *testing.T) { { name: "incrbyfloat decr value", before: func(t *testing.T) { - assert.Equal(t, false, cache.add("test", 3.1)) + assert.Equal(t, true, cache.add("test", 3.1)) }, after: func(t *testing.T) { assert.Equal(t, true, cache.remove("test")) @@ -908,7 +912,7 @@ func TestCache_IncrByFloat(t *testing.T) { { name: "incrbyfloat value type error", before: func(t *testing.T) { - assert.Equal(t, false, cache.add("test", "hello")) + assert.Equal(t, true, cache.add("test", "hello")) }, after: func(t *testing.T) { assert.Equal(t, true, cache.remove("test")) diff --git a/memory/lru/list.go b/memory/lru/list.go index 9b38ae6..a441d76 100644 --- a/memory/lru/list.go +++ b/memory/lru/list.go @@ -55,14 +55,14 @@ func (l *linkedList[T]) len() int { return l.capacity } -func (l *linkedList[T]) Front() *element[T] { +func (l *linkedList[T]) front() *element[T] { if l.capacity == 0 { return nil } return l.root.next } -func (l *linkedList[T]) Back() *element[T] { +func (l *linkedList[T]) back() *element[T] { if l.capacity == 0 { return nil } @@ -111,7 +111,7 @@ func (l *linkedList[T]) move(e, at *element[T]) { e.next.prev = e } -func (l *linkedList[T]) Remove(e *element[T]) any { +func (l *linkedList[T]) removeElem(e *element[T]) any { if e.list == l { l.remove(e) } @@ -172,7 +172,7 @@ func (l *linkedList[T]) insertAfter(v T, mark *element[T]) *element[T] { func (l *linkedList[T]) pushBackList(other *linkedList[T]) { l.lazyInit() - e := other.Front() + e := other.front() for i := other.len(); i > 0; i-- { l.insertValue(e.Value, l.root.prev) e = e.nextElem() @@ -181,7 +181,7 @@ func (l *linkedList[T]) pushBackList(other *linkedList[T]) { func (l *linkedList[T]) pushFrontList(other *linkedList[T]) { l.lazyInit() - for i, e := other.len(), other.Back(); i > 0; i, e = i-1, e.prevElem() { + for i, e := other.len(), other.back(); i > 0; i, e = i-1, e.prevElem() { l.insertValue(e.Value, &l.root) } } diff --git a/memory/lru/list_test.go b/memory/lru/list_test.go index 8793c7d..1bd9fe4 100644 --- a/memory/lru/list_test.go +++ b/memory/lru/list_test.go @@ -25,7 +25,7 @@ func Example() { e1 := l.pushFront(1) l.insertBefore(3, e4) l.insertAfter(2, e1) - for e := l.Front(); e != nil; e = e.nextElem() { + for e := l.front(); e != nil; e = e.nextElem() { fmt.Println(e.Value) } @@ -96,7 +96,7 @@ func TestLinkedList(t *testing.T) { checkLinkedListPointers(t, l, []*element[any]{e}) l.moveToBack(e) checkLinkedListPointers(t, l, []*element[any]{e}) - l.Remove(e) + l.removeElem(e) checkLinkedListPointers(t, l, []*element[any]{}) e2 := l.pushFront(2) @@ -105,7 +105,7 @@ func TestLinkedList(t *testing.T) { e4 := l.pushBack("banana") checkLinkedListPointers(t, l, []*element[any]{e1, e2, e3, e4}) - l.Remove(e2) + l.removeElem(e2) checkLinkedListPointers(t, l, []*element[any]{e1, e3, e4}) l.moveToFront(e3) @@ -127,26 +127,26 @@ func TestLinkedList(t *testing.T) { e2 = l.insertBefore(2, e1) checkLinkedListPointers(t, l, []*element[any]{e2, e1, e4, e3}) - l.Remove(e2) + l.removeElem(e2) e2 = l.insertBefore(2, e4) checkLinkedListPointers(t, l, []*element[any]{e1, e2, e4, e3}) - l.Remove(e2) + l.removeElem(e2) e2 = l.insertBefore(2, e3) checkLinkedListPointers(t, l, []*element[any]{e1, e4, e2, e3}) - l.Remove(e2) + l.removeElem(e2) e2 = l.insertAfter(2, e1) checkLinkedListPointers(t, l, []*element[any]{e1, e2, e4, e3}) - l.Remove(e2) + l.removeElem(e2) e2 = l.insertAfter(2, e4) checkLinkedListPointers(t, l, []*element[any]{e1, e4, e2, e3}) - l.Remove(e2) + l.removeElem(e2) e2 = l.insertAfter(2, e3) checkLinkedListPointers(t, l, []*element[any]{e1, e4, e3, e2}) - l.Remove(e2) + l.removeElem(e2) sum := 0 - for e := l.Front(); e != nil; e = e.nextElem() { + for e := l.front(); e != nil; e = e.nextElem() { if i, ok := e.Value.(int); ok { sum += i } @@ -156,9 +156,9 @@ func TestLinkedList(t *testing.T) { } var next *element[any] - for e := l.Front(); e != nil; e = next { + for e := l.front(); e != nil; e = next { next = e.nextElem() - l.Remove(e) + l.removeElem(e) } checkLinkedListPointers(t, l, []*element[any]{}) } @@ -169,7 +169,7 @@ func checkLinkedList[T int](t *testing.T, l *linkedList[T], es []any) { } i := 0 - for e := l.Front(); e != nil; e = e.nextElem() { + for e := l.front(); e != nil; e = e.nextElem() { le := e.Value if le != es[i] { t.Errorf("elt[%d].Value = %v, want %v", i, le, es[i]) @@ -228,10 +228,10 @@ func TestRemoveEle(t *testing.T) { e1 := l.pushBack(1) e2 := l.pushBack(2) checkLinkedListPointers(t, l, []*element[int]{e1, e2}) - e := l.Front() - l.Remove(e) + e := l.front() + l.removeElem(e) checkLinkedListPointers(t, l, []*element[int]{e2}) - l.Remove(e) + l.removeElem(e) checkLinkedListPointers(t, l, []*element[int]{e2}) } @@ -244,8 +244,8 @@ func TestIssue4103Ele(t *testing.T) { l2.pushBack(3) l2.pushBack(4) - e := l1.Front() - l2.Remove(e) + e := l1.front() + l2.removeElem(e) if n := l2.len(); n != 2 { t.Errorf("l2.len() = %d, want 2", n) } @@ -261,8 +261,8 @@ func TestIssue6349Ele(t *testing.T) { l.pushBack(1) l.pushBack(2) - e := l.Front() - l.Remove(e) + e := l.front() + l.removeElem(e) if e.Value != 1 { t.Errorf("e.value = %d, want 1", e.Value) } From 7870691966fbcf1a4a74b4107a7d94a900daadd8 Mon Sep 17 00:00:00 2001 From: Stone-afk <1711865140@qq.com> Date: Sun, 31 Dec 2023 22:22:38 +0800 Subject: [PATCH 11/16] feature add lru --- memory/lru/cache.go | 3 +- memory/lru/list.go | 88 +++++++--------------------- memory/lru/list_test.go | 124 ++++++++++------------------------------ 3 files changed, 53 insertions(+), 162 deletions(-) diff --git a/memory/lru/cache.go b/memory/lru/cache.go index 1caaa51..3828959 100644 --- a/memory/lru/cache.go +++ b/memory/lru/cache.go @@ -154,11 +154,12 @@ func (c *Cache) delete(key string) { func (c *Cache) len() int { var length int - for elem := c.list.back(); elem != nil; elem = elem.prevElem() { + for elem, i := c.list.back(), 0; i < c.list.len(); i++ { if elem.Value.isExpired() { c.removeElement(elem) continue } + elem = elem.prev length++ } return length diff --git a/memory/lru/list.go b/memory/lru/list.go index a441d76..5b94845 100644 --- a/memory/lru/list.go +++ b/memory/lru/list.go @@ -16,39 +16,23 @@ package lru type element[T any] struct { Value T - list *linkedList[T] next, prev *element[T] } -func (e *element[T]) nextElem() *element[T] { - if n := e.next; e.list != nil && n != &e.list.root { - return n - } - return nil -} - -func (e *element[T]) prevElem() *element[T] { - if p := e.prev; e.list != nil && p != &e.list.root { - return p - } - return nil -} - type linkedList[T any] struct { - root element[T] + head *element[T] + tail *element[T] capacity int } func newLinkedList[T any]() *linkedList[T] { - l := &linkedList[T]{} - return l.init() -} - -func (l *linkedList[T]) init() *linkedList[T] { - l.root.next = &l.root - l.root.prev = &l.root - l.capacity = 0 - return l + head := &element[T]{} + tail := &element[T]{next: head, prev: head} + head.next, head.prev = tail, tail + return &linkedList[T]{ + head: head, + tail: tail, + } } func (l *linkedList[T]) len() int { @@ -59,20 +43,14 @@ func (l *linkedList[T]) front() *element[T] { if l.capacity == 0 { return nil } - return l.root.next + return l.head.next } func (l *linkedList[T]) back() *element[T] { if l.capacity == 0 { return nil } - return l.root.prev -} - -func (l *linkedList[T]) lazyInit() { - if l.root.next == nil { - l.init() - } + return l.tail.prev } func (l *linkedList[T]) insert(e, at *element[T]) *element[T] { @@ -80,7 +58,6 @@ func (l *linkedList[T]) insert(e, at *element[T]) *element[T] { e.next = at.next e.prev.next = e e.next.prev = e - e.list = l l.capacity++ return e } @@ -94,7 +71,6 @@ func (l *linkedList[T]) remove(e *element[T]) { e.next.prev = e.prev e.next = nil e.prev = nil - e.list = nil l.capacity-- } @@ -112,76 +88,56 @@ func (l *linkedList[T]) move(e, at *element[T]) { } func (l *linkedList[T]) removeElem(e *element[T]) any { - if e.list == l { - l.remove(e) - } + l.remove(e) return e.Value } func (l *linkedList[T]) pushFront(v T) *element[T] { - l.lazyInit() - return l.insertValue(v, &l.root) + return l.insertValue(v, l.head) } func (l *linkedList[T]) pushBack(v T) *element[T] { - l.lazyInit() - return l.insertValue(v, l.root.prev) + return l.insertValue(v, l.tail.prev) } func (l *linkedList[T]) moveToFront(e *element[T]) { - if e.list != l || l.root.next == e { - return - } - l.move(e, &l.root) + l.move(e, l.head) } func (l *linkedList[T]) moveToBack(e *element[T]) { - if e.list != l || l.root.prev == e { - return - } - l.move(e, l.root.prev) + l.move(e, l.tail.prev) } func (l *linkedList[T]) moveBefore(e, mark *element[T]) { - if e.list != l || e == mark || mark.list != l { - return - } l.move(e, mark.prev) } func (l *linkedList[T]) moveAfter(e, mark *element[T]) { - if e.list != l || e == mark || mark.list != l { + if e == mark { return } l.move(e, mark) } func (l *linkedList[T]) insertBefore(v T, mark *element[T]) *element[T] { - if mark.list != l { - return nil - } return l.insertValue(v, mark.prev) } func (l *linkedList[T]) insertAfter(v T, mark *element[T]) *element[T] { - if mark.list != l { - return nil - } return l.insertValue(v, mark) } func (l *linkedList[T]) pushBackList(other *linkedList[T]) { - l.lazyInit() e := other.front() for i := other.len(); i > 0; i-- { - l.insertValue(e.Value, l.root.prev) - e = e.nextElem() + l.insertValue(e.Value, l.tail.prev) + e = e.next } } func (l *linkedList[T]) pushFrontList(other *linkedList[T]) { - l.lazyInit() - for i, e := other.len(), other.back(); i > 0; i, e = i-1, e.prevElem() { - l.insertValue(e.Value, &l.root) + for i, e := other.len(), other.back(); i > 0; i-- { + l.insertValue(e.Value, l.head) + e = e.prev } } diff --git a/memory/lru/list_test.go b/memory/lru/list_test.go index 1bd9fe4..eb1e5d5 100644 --- a/memory/lru/list_test.go +++ b/memory/lru/list_test.go @@ -25,8 +25,9 @@ func Example() { e1 := l.pushFront(1) l.insertBefore(3, e4) l.insertAfter(2, e1) - for e := l.front(); e != nil; e = e.nextElem() { + for e, i := l.front(), 0; i < l.capacity; i++ { fmt.Println(e.Value) + e = e.next } // Output: @@ -45,15 +46,15 @@ func checkLinkedListLen[T any](t *testing.T, l *linkedList[T], len int) bool { } func checkLinkedListPointers[T any](t *testing.T, l *linkedList[T], es []*element[T]) { - root := &l.root + root := l.head if !checkLinkedListLen[T](t, l, len(es)) { return } if len(es) == 0 { - if l.root.next != nil && l.root.next != root || l.root.prev != nil && l.root.prev != root { - t.Errorf("l.root.next = %p, l.root.prev = %p; both should both be nil or %p", l.root.next, l.root.prev, root) + if l.head.next != l.tail && l.head.next != root || l.tail.prev != root { + t.Errorf("l.head.next = %p, l.tail.prev = %p; both should both be nil or %p", l.head.next, l.tail.prev, root) } return } @@ -65,11 +66,11 @@ func checkLinkedListPointers[T any](t *testing.T, l *linkedList[T], es []*elemen prev = es[i-1] Prev = prev } - if p := e.prev; p != prev { + if p := e.prev; p != root && p != prev { t.Errorf("elt[%d](%p).prev = %p, want %p", i, e, p, prev) } - if p := e.prevElem(); p != Prev { - t.Errorf("elt[%d](%p).prevElem() = %p, want %p", i, e, p, Prev) + if p := e.prev; p != root && p != Prev { + t.Errorf("elt[%d](%p).prev = %p, want %p", i, e, p, Prev) } next := root @@ -78,11 +79,11 @@ func checkLinkedListPointers[T any](t *testing.T, l *linkedList[T], es []*elemen next = es[i+1] Next = next } - if n := e.next; n != next { + if n := e.next; n != l.tail && n != next { t.Errorf("elt[%d](%p).next = %p, want %p", i, e, n, next) } - if n := e.nextElem(); n != Next { - t.Errorf("elt[%d](%p).nextElem() = %p, want %p", i, e, n, Next) + if n := e.next; n != l.tail && n != Next { + t.Errorf("elt[%d](%p).next = %p, want %p", i, e, n, Next) } } } @@ -146,19 +147,22 @@ func TestLinkedList(t *testing.T) { l.removeElem(e2) sum := 0 - for e := l.front(); e != nil; e = e.nextElem() { + for e, i := l.front(), 0; i < l.capacity; i++ { if i, ok := e.Value.(int); ok { sum += i } + e = e.next } if sum != 4 { t.Errorf("sum over l = %d, want 4", sum) } - var next *element[any] - for e := l.front(); e != nil; e = next { - next = e.nextElem() + //var next *element[any] + capacity := l.capacity + for e, i := l.front(), 0; i < capacity; i++ { + next := e.next l.removeElem(e) + e = next } checkLinkedListPointers(t, l, []*element[any]{}) } @@ -169,12 +173,14 @@ func checkLinkedList[T int](t *testing.T, l *linkedList[T], es []any) { } i := 0 - for e := l.front(); e != nil; e = e.nextElem() { - le := e.Value - if le != es[i] { - t.Errorf("elt[%d].Value = %v, want %v", i, le, es[i]) + for e := l.front(); i < l.capacity; i++ { + if e != l.tail { + le := e.Value + if le != es[i] { + t.Errorf("elt[%d].Value = %v, want %v", i, le, es[i]) + } + e = e.next } - i++ } } @@ -231,29 +237,9 @@ func TestRemoveEle(t *testing.T) { e := l.front() l.removeElem(e) checkLinkedListPointers(t, l, []*element[int]{e2}) + e = l.front() l.removeElem(e) - checkLinkedListPointers(t, l, []*element[int]{e2}) -} - -func TestIssue4103Ele(t *testing.T) { - l1 := newLinkedList[int]() - l1.pushBack(1) - l1.pushBack(2) - - l2 := newLinkedList[int]() - l2.pushBack(3) - l2.pushBack(4) - - e := l1.front() - l2.removeElem(e) - if n := l2.len(); n != 2 { - t.Errorf("l2.len() = %d, want 2", n) - } - - l1.insertBefore(8, e) - if n := l1.len(); n != 3 { - t.Errorf("l1.len() = %d, want 3", n) - } + checkLinkedListPointers(t, l, []*element[int]{}) } func TestIssue6349Ele(t *testing.T) { @@ -266,10 +252,10 @@ func TestIssue6349Ele(t *testing.T) { if e.Value != 1 { t.Errorf("e.value = %d, want 1", e.Value) } - if e.nextElem() != nil { + if e.next != nil && e.next != l.tail { t.Errorf("e.nextElem() != nil") } - if e.prevElem() != nil { + if e.prev != nil && e.prev != l.head { t.Errorf("e.prevElem() != nil") } } @@ -306,55 +292,3 @@ func TestMoveEle(t *testing.T) { l.moveAfter(e2, e3) checkLinkedListPointers(t, l, []*element[int]{e1, e3, e2, e4}) } - -func TestZeroLinkedList(t *testing.T) { - var l1 = new(linkedList[int]) - l1.pushFront(1) - checkLinkedList(t, l1, []any{1}) - - var l2 = new(linkedList[int]) - l2.pushBack(1) - checkLinkedList(t, l2, []any{1}) - - var l3 = new(linkedList[int]) - l3.pushFrontList(l1) - checkLinkedList(t, l3, []any{1}) - - var l4 = new(linkedList[int]) - l4.pushBackList(l2) - checkLinkedList(t, l4, []any{1}) -} - -func TestInsertBeforeUnknownMarkEle(t *testing.T) { - var l linkedList[int] - l.pushBack(1) - l.pushBack(2) - l.pushBack(3) - l.insertBefore(1, new(element[int])) - checkLinkedList(t, &l, []any{1, 2, 3}) -} - -func TestInsertAfterUnknownMarkEle(t *testing.T) { - var l linkedList[int] - l.pushBack(1) - l.pushBack(2) - l.pushBack(3) - l.insertAfter(1, new(element[int])) - checkLinkedList(t, &l, []any{1, 2, 3}) -} - -func TestMoveUnknownMarkEle(t *testing.T) { - var l1 linkedList[int] - e1 := l1.pushBack(1) - - var l2 linkedList[int] - e2 := l2.pushBack(2) - - l1.moveAfter(e1, e2) - checkLinkedList(t, &l1, []any{1}) - checkLinkedList(t, &l2, []any{2}) - - l1.moveBefore(e1, e2) - checkLinkedList(t, &l1, []any{1}) - checkLinkedList(t, &l2, []any{2}) -} From 85c6e8acca9edd0f1163237dc37ccf5f70d2e34a Mon Sep 17 00:00:00 2001 From: Stone-afk <1711865140@qq.com> Date: Tue, 2 Jan 2024 16:28:32 +0800 Subject: [PATCH 12/16] feature add lru --- memory/lru/cache.go | 48 ++++++++++++++++++++++++++++++++-------- memory/lru/cache_test.go | 9 ++++++++ 2 files changed, 48 insertions(+), 9 deletions(-) diff --git a/memory/lru/cache.go b/memory/lru/cache.go index 3828959..92dfd42 100644 --- a/memory/lru/cache.go +++ b/memory/lru/cache.go @@ -53,28 +53,58 @@ func WithEvictCallback(callback func(k string, v any)) Option { } } +func WithCycleInterval(interval time.Duration) Option { + return func(l *Cache) { + l.cycleInterval = interval + } +} + type Cache struct { - lock sync.RWMutex - capacity int - list *linkedList[entry] - data map[string]*element[entry] - callback EvictCallback + lock sync.RWMutex + capacity int + list *linkedList[entry] + data map[string]*element[entry] + callback EvictCallback + cycleInterval time.Duration } func NewCache(capacity int, options ...Option) *Cache { res := &Cache{ - list: newLinkedList[entry](), - data: make(map[string]*element[entry], capacity), - capacity: capacity, + 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 + limit := c.list.len() / 3 + c.lock.Lock() + for elem, i := c.list.back(), 0; i < c.list.len(); i++ { + if elem.Value.isExpired() { + c.removeElement(elem) + } + cnt++ + if cnt >= limit { + break + } + } + c.lock.Unlock() + } + }() +} + func (c *Cache) pushEntry(key string, ent entry) bool { - if c.len() > c.capacity { + if len(c.data) >= c.capacity && c.len() >= c.capacity { c.removeOldest() } if elem, ok := c.data[key]; ok { diff --git a/memory/lru/cache_test.go b/memory/lru/cache_test.go index dfb4b08..7ddf6b3 100644 --- a/memory/lru/cache_test.go +++ b/memory/lru/cache_test.go @@ -29,6 +29,15 @@ import ( "github.com/stretchr/testify/require" ) +func TestLocalCache_cleanCycle(t *testing.T) { + c := NewCache(200, WithCycleInterval(time.Second)) + err := c.Set(context.Background(), "key1", "value1", time.Millisecond*100) + require.NoError(t, err) + time.Sleep(time.Second * 3) + val := c.Get(context.Background(), "key1") + assert.Equal(t, errs.ErrKeyNotExist, val.Err) +} + func TestCache_Set(t *testing.T) { evictCounter := 0 onEvicted := func(key string, value any) { From 13b3beaae8015a54e9499f42ae8d9cca74b92436 Mon Sep 17 00:00:00 2001 From: Stone-afk <1711865140@qq.com> Date: Fri, 5 Jan 2024 14:04:46 +0800 Subject: [PATCH 13/16] feature: add lru --- memory/lru/cache.go | 6 ++++ memory/lru/cache_test.go | 78 +++++++++++++++++++++++++++++++++++++--- 2 files changed, 79 insertions(+), 5 deletions(-) diff --git a/memory/lru/cache.go b/memory/lru/cache.go index 92dfd42..3721c5e 100644 --- a/memory/lru/cache.go +++ b/memory/lru/cache.go @@ -93,6 +93,7 @@ func (c *Cache) cleanCycle() { if elem.Value.isExpired() { c.removeElement(elem) } + elem = elem.prev cnt++ if cnt >= limit { break @@ -105,6 +106,11 @@ func (c *Cache) cleanCycle() { 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() } if elem, ok := c.data[key]; ok { diff --git a/memory/lru/cache_test.go b/memory/lru/cache_test.go index 7ddf6b3..041adce 100644 --- a/memory/lru/cache_test.go +++ b/memory/lru/cache_test.go @@ -31,11 +31,79 @@ import ( func TestLocalCache_cleanCycle(t *testing.T) { c := NewCache(200, WithCycleInterval(time.Second)) - err := c.Set(context.Background(), "key1", "value1", time.Millisecond*100) - require.NoError(t, err) - time.Sleep(time.Second * 3) - val := c.Get(context.Background(), "key1") - assert.Equal(t, errs.ErrKeyNotExist, val.Err) + + testCase := []struct { + name string + before func(t *testing.T) + after func(t *testing.T) + + key string + + wantVal string + wantErr error + }{ + { + name: "tail exist TTL value", + before: func(t *testing.T) { + ctx := context.Background() + err := c.Set(ctx, "test1", "hello1", time.Second) + assert.Nil(t, err) + err = c.Set(ctx, "test2", "hello2", time.Second*10) + assert.Nil(t, err) + }, + after: func(t *testing.T) { + ctx := context.Background() + _, err := c.Delete(ctx, "test1", "test2") + assert.Nil(t, err) + }, + key: "test1", + wantVal: "", + wantErr: errs.ErrKeyNotExist, + }, + { + name: "not exist TTL value", + before: func(t *testing.T) { + _ = c.add("test1", "hello1") + }, + after: func(t *testing.T) { + _, err := c.Delete(context.Background(), "test1") + assert.Nil(t, err) + }, + key: "test1", + wantVal: "hello1", + }, + { + name: "exist TTL value", + before: func(t *testing.T) { + ctx := context.Background() + err := c.Set(ctx, "test1", "hello1", time.Second*10) + assert.Nil(t, err) + err = c.Set(ctx, "test2", "hello2", time.Second) + assert.Nil(t, err) + }, + after: func(t *testing.T) { + ctx := context.Background() + _, err := c.Delete(ctx, "test1", "test2") + assert.Nil(t, err) + }, + key: "test2", + wantVal: "", + wantErr: errs.ErrKeyNotExist, + }, + } + for _, tc := range testCase { + t.Run(tc.name, func(t *testing.T) { + tc.before(t) + time.Sleep(time.Second) + ctx, cancelFunc := context.WithTimeout(context.Background(), time.Second*5) + defer cancelFunc() + result := c.Get(ctx, tc.key) + val, err := result.String() + assert.Equal(t, tc.wantVal, val) + assert.Equal(t, tc.wantErr, err) + tc.after(t) + }) + } } func TestCache_Set(t *testing.T) { From ad96dbdf2902cf820c668dbde483499a8284f160 Mon Sep 17 00:00:00 2001 From: Stone-afk <1711865140@qq.com> Date: Thu, 11 Jan 2024 21:55:48 +0800 Subject: [PATCH 14/16] feature: add lru --- memory/lru/cache.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/memory/lru/cache.go b/memory/lru/cache.go index 3721c5e..53cd210 100644 --- a/memory/lru/cache.go +++ b/memory/lru/cache.go @@ -87,8 +87,8 @@ func (c *Cache) cleanCycle() { ticker := time.NewTicker(c.cycleInterval) for range ticker.C { cnt := 0 - limit := c.list.len() / 3 c.lock.Lock() + limit := c.list.len() / 3 for elem, i := c.list.back(), 0; i < c.list.len(); i++ { if elem.Value.isExpired() { c.removeElement(elem) From 083d4f7a09e40c50aea260b9210dc452023110ad Mon Sep 17 00:00:00 2001 From: Stone-afk <1711865140@qq.com> Date: Thu, 11 Jan 2024 22:09:26 +0800 Subject: [PATCH 15/16] feature: add lru --- memory/lru/cache_test.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/memory/lru/cache_test.go b/memory/lru/cache_test.go index 041adce..b22649d 100644 --- a/memory/lru/cache_test.go +++ b/memory/lru/cache_test.go @@ -63,7 +63,11 @@ func TestLocalCache_cleanCycle(t *testing.T) { { name: "not exist TTL value", before: func(t *testing.T) { - _ = c.add("test1", "hello1") + ctx := context.Background() + err := c.Set(ctx, "test1", "hello1", time.Second) + assert.Nil(t, err) + res := c.GetSet(ctx, "test1", "hello1") + assert.Nil(t, res.Err) }, after: func(t *testing.T) { _, err := c.Delete(context.Background(), "test1") From 799b8a2a7913a377f8fed07462342ba5f3d7142a Mon Sep 17 00:00:00 2001 From: Stone-afk <1711865140@qq.com> Date: Thu, 11 Jan 2024 22:33:40 +0800 Subject: [PATCH 16/16] feature: add lru --- memory/lru/cache.go | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/memory/lru/cache.go b/memory/lru/cache.go index 53cd210..2ab2883 100644 --- a/memory/lru/cache.go +++ b/memory/lru/cache.go @@ -162,13 +162,10 @@ func (c *Cache) removeElement(elem *element[entry]) { } } -func (c *Cache) remove(key string) (present bool) { +func (c *Cache) remove(key string) bool { if elem, ok := c.data[key]; ok { c.removeElement(elem) - if elem.Value.isExpired() { - return false - } - return true + return !elem.Value.isExpired() } return false }