From da8d024407e8406ec279dfc9d20aa92d461a1e0b Mon Sep 17 00:00:00 2001 From: Longyue Li Date: Tue, 10 Oct 2023 14:56:37 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0Delete=E6=8E=A5?= =?UTF-8?q?=E5=8F=A3=E3=80=81=E5=85=B7=E4=BD=93=E5=AE=9E=E7=8E=B0=E5=8F=8A?= =?UTF-8?q?=E6=B5=8B=E8=AF=95=E3=80=81=E6=B7=BB=E5=8A=A0=E5=90=AF=E5=8A=A8?= =?UTF-8?q?=E7=AC=AC=E4=B8=89=E6=96=B9=E7=8E=AF=E5=A2=83=E7=9A=84=E5=91=BD?= =?UTF-8?q?=E4=BB=A4=E7=AD=89=20(#25)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 添加Delete接口、具体实现及测试、添加启动第三方环境的命令等 Signed-off-by: longyue0521 * refactor: 将TestCache_e2e_Delete改为TestCache_Delete Signed-off-by: longyue0521 --------- Signed-off-by: longyue0521 --- Makefile | 10 ++- internal/errs/errs.go | 1 + memory/lru/cache.go | 27 +++++++ memory/lru/cache_test.go | 114 ++++++++++++++++++++++++++++- redis/cache.go | 4 + redis/cache_e2e_test.go | 154 ++++++++++++++++++++++++++++++--------- redis/cache_test.go | 112 ++++++++++++++++++++++++++++ script/integrate_test.sh | 4 +- types.go | 10 ++- 9 files changed, 396 insertions(+), 40 deletions(-) diff --git a/Makefile b/Makefile index 5b5a103..fa7c011 100644 --- a/Makefile +++ b/Makefile @@ -28,4 +28,12 @@ check: # e2e 测试 .PHONY: e2e e2e: - sh ./script/integrate_test.sh \ No newline at end of file + sh ./script/integrate_test.sh + +.PHONY: dev_3rd_up +dev_3rd_up: + docker-compose -f script/integration_test_compose.yml up -d + +.PHONY: dev_3rd_down +dev_3rd_down: + docker-compose -f script/integration_test_compose.yml down -v \ No newline at end of file diff --git a/internal/errs/errs.go b/internal/errs/errs.go index 9dcec89..a15ec5c 100644 --- a/internal/errs/errs.go +++ b/internal/errs/errs.go @@ -17,3 +17,4 @@ package errs import "errors" var ErrKeyNotExist = errors.New("key 不存在") +var ErrDeleteKeyFailed = errors.New("删除key失败") diff --git a/memory/lru/cache.go b/memory/lru/cache.go index e73406f..cf525fd 100644 --- a/memory/lru/cache.go +++ b/memory/lru/cache.go @@ -17,6 +17,7 @@ package lru import ( "context" "errors" + "fmt" "sync" "time" @@ -27,6 +28,10 @@ import ( "github.com/hashicorp/golang-lru/v2/simplelru" ) +var ( + _ ecache.Cache = (*Cache)(nil) +) + type Cache struct { lock sync.RWMutex client simplelru.LRUCache[string, any] @@ -89,6 +94,28 @@ func (c *Cache) GetSet(ctx context.Context, key string, val string) (result ecac return } +func (c *Cache) 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.client.Get(k) + if !ok { + continue + } + if c.client.Remove(k) { + n++ + } else { + return n, fmt.Errorf("%w: key = %s", errs.ErrDeleteKeyFailed, k) + } + } + return n, nil +} + // anySliceToValueSlice 公共转换 func (c *Cache) anySliceToValueSlice(data ...any) []ecache.Value { newVal := make([]ecache.Value, len(data), cap(data)) diff --git a/memory/lru/cache_test.go b/memory/lru/cache_test.go index 1787d51..da8511c 100644 --- a/memory/lru/cache_test.go +++ b/memory/lru/cache_test.go @@ -21,10 +21,9 @@ import ( "time" "github.com/ecodeclub/ecache" + "github.com/ecodeclub/ecache/internal/errs" "github.com/ecodeclub/ekit/list" "github.com/hashicorp/golang-lru/v2/simplelru" - - "github.com/ecodeclub/ecache/internal/errs" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -251,6 +250,101 @@ func TestCache_GetSet(t *testing.T) { } } +func TestCache_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 single existed 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: 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 existed 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: 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 existed 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: 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 TestCache_LPush(t *testing.T) { evictCounter := 0 onEvicted := func(key string, value any) { @@ -628,3 +722,19 @@ 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 +} + +func newSimpleLRUClient(size int) (simplelru.LRUCache[string, any], error) { + evictCounter := 0 + onEvicted := func(key string, value any) { + evictCounter++ + } + return simplelru.NewLRU[string, any](size, onEvicted) +} diff --git a/redis/cache.go b/redis/cache.go index b57c5ec..1b0ce1d 100644 --- a/redis/cache.go +++ b/redis/cache.go @@ -42,6 +42,10 @@ func (c *Cache) SetNX(ctx context.Context, key string, val any, expiration time. return c.client.SetNX(ctx, key, val, expiration).Result() } +func (c *Cache) Delete(ctx context.Context, key ...string) (int64, error) { + return c.client.Del(ctx, key...).Result() +} + func (c *Cache) Get(ctx context.Context, key string) (val ecache.Value) { val.Val, val.Err = c.client.Get(ctx, key).Result() if val.Err != nil && errors.Is(val.Err, redis.Nil) { diff --git a/redis/cache_e2e_test.go b/redis/cache_e2e_test.go index 7efabab..5fe923e 100644 --- a/redis/cache_e2e_test.go +++ b/redis/cache_e2e_test.go @@ -21,6 +21,7 @@ import ( "testing" "time" + "github.com/ecodeclub/ecache" "github.com/ecodeclub/ecache/internal/errs" "github.com/redis/go-redis/v9" "github.com/stretchr/testify/assert" @@ -28,9 +29,7 @@ import ( ) func TestCache_e2e_Set(t *testing.T) { - rdb := redis.NewClient(&redis.Options{ - Addr: "localhost:6379", - }) + rdb := newRedisClient() require.NoError(t, rdb.Ping(context.Background()).Err()) testCases := []struct { @@ -73,9 +72,7 @@ func TestCache_e2e_Set(t *testing.T) { } func TestCache_e2e_Get(t *testing.T) { - rdb := redis.NewClient(&redis.Options{ - Addr: "localhost:6379", - }) + rdb := newRedisClient() require.NoError(t, rdb.Ping(context.Background()).Err()) testCases := []struct { @@ -127,10 +124,103 @@ func TestCache_e2e_Get(t *testing.T) { } } +func TestCache_e2e_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 single existed 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: 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 existed 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: 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 existed 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: 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 TestCache_e2e_SetNX(t *testing.T) { - rdb := redis.NewClient(&redis.Options{ - Addr: "localhost:6379", - }) + rdb := newRedisClient() require.NoError(t, rdb.Ping(context.Background()).Err()) testCase := []struct { @@ -182,9 +272,7 @@ func TestCache_e2e_SetNX(t *testing.T) { } func TestCache_e2e_GetSet(t *testing.T) { - rdb := redis.NewClient(&redis.Options{ - Addr: "localhost:6379", - }) + rdb := newRedisClient() require.NoError(t, rdb.Ping(context.Background()).Err()) testCase := []struct { @@ -241,9 +329,7 @@ func TestCache_e2e_GetSet(t *testing.T) { } func TestCache_e2e_LPush(t *testing.T) { - rdb := redis.NewClient(&redis.Options{ - Addr: "localhost:6379", - }) + rdb := newRedisClient() require.NoError(t, rdb.Ping(context.Background()).Err()) testCase := []struct { @@ -295,9 +381,7 @@ func TestCache_e2e_LPush(t *testing.T) { } func TestCache_e2e_LPop(t *testing.T) { - rdb := redis.NewClient(&redis.Options{ - Addr: "localhost:6379", - }) + rdb := newRedisClient() require.NoError(t, rdb.Ping(context.Background()).Err()) testCase := []struct { @@ -358,9 +442,7 @@ func TestCache_e2e_LPop(t *testing.T) { } func TestCache_e2e_SAdd(t *testing.T) { - rdb := redis.NewClient(&redis.Options{ - Addr: "localhost:6379", - }) + rdb := newRedisClient() require.NoError(t, rdb.Ping(context.Background()).Err()) testCase := []struct { @@ -413,9 +495,7 @@ func TestCache_e2e_SAdd(t *testing.T) { } func TestCache_e2e_SRem(t *testing.T) { - rdb := redis.NewClient(&redis.Options{ - Addr: "localhost:6379", - }) + rdb := newRedisClient() require.NoError(t, rdb.Ping(context.Background()).Err()) testCase := []struct { @@ -506,9 +586,7 @@ func TestCache_e2e_SRem(t *testing.T) { } func TestCache_e2e_IncrBy(t *testing.T) { - rdb := redis.NewClient(&redis.Options{ - Addr: "localhost:6379", - }) + rdb := newRedisClient() require.NoError(t, rdb.Ping(context.Background()).Err()) testCase := []struct { @@ -576,9 +654,7 @@ func TestCache_e2e_IncrBy(t *testing.T) { } func TestCache_e2e_DecrBy(t *testing.T) { - rdb := redis.NewClient(&redis.Options{ - Addr: "localhost:6379", - }) + rdb := newRedisClient() require.NoError(t, rdb.Ping(context.Background()).Err()) testCase := []struct { @@ -646,9 +722,7 @@ func TestCache_e2e_DecrBy(t *testing.T) { } func TestCache_e2e_IncrByFloat(t *testing.T) { - rdb := redis.NewClient(&redis.Options{ - Addr: "localhost:6379", - }) + rdb := newRedisClient() require.NoError(t, rdb.Ping(context.Background()).Err()) testCase := []struct { @@ -725,3 +799,17 @@ func TestCache_e2e_IncrByFloat(t *testing.T) { } } + +func newCache() (ecache.Cache, error) { + rdb := newRedisClient() + if err := rdb.Ping(context.Background()).Err(); err != nil { + return nil, err + } + return NewCache(rdb), nil +} + +func newRedisClient() *redis.Client { + return redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + }) +} diff --git a/redis/cache_test.go b/redis/cache_test.go index 70ec4c6..9f25629 100644 --- a/redis/cache_test.go +++ b/redis/cache_test.go @@ -247,6 +247,118 @@ func TestCache_GetSet(t *testing.T) { } } +func TestCache_Delete(t *testing.T) { + testCases := []struct { + name string + + mock func(*gomock.Controller) redis.Cmdable + + key []string + + wantN int64 + wantErr error + }{ + { + name: "delete single existed key", + mock: func(ctrl *gomock.Controller) redis.Cmdable { + cmd := mocks.NewMockCmdable(ctrl) + status := redis.NewIntCmd(context.Background()) + status.SetVal(int64(1)) + status.SetErr(nil) + cmd.EXPECT(). + Del(context.Background(), gomock.Any()). + Return(status) + return cmd + }, + key: []string{"name"}, + wantN: 1, + }, + { + name: "delete single does not existed key", + mock: func(ctrl *gomock.Controller) redis.Cmdable { + cmd := mocks.NewMockCmdable(ctrl) + status := redis.NewIntCmd(context.Background()) + status.SetVal(int64(0)) + status.SetErr(nil) + cmd.EXPECT(). + Del(context.Background(), gomock.Any()). + Return(status) + return cmd + }, + key: []string{"name"}, + }, + { + name: "delete multiple existed keys", + mock: func(ctrl *gomock.Controller) redis.Cmdable { + cmd := mocks.NewMockCmdable(ctrl) + status := redis.NewIntCmd(context.Background()) + status.SetVal(int64(2)) + status.SetErr(nil) + cmd.EXPECT(). + Del(context.Background(), gomock.Any(), gomock.Any()). + Return(status) + return cmd + }, + key: []string{"name", "age"}, + wantN: 2, + }, + { + name: "delete multiple do not existed keys", + mock: func(ctrl *gomock.Controller) redis.Cmdable { + cmd := mocks.NewMockCmdable(ctrl) + status := redis.NewIntCmd(context.Background()) + status.SetVal(0) + status.SetErr(nil) + cmd.EXPECT(). + Del(context.Background(), gomock.Any(), gomock.Any()). + Return(status) + return cmd + }, + key: []string{"name", "age"}, + }, + { + name: "delete multiple keys, some do not existed keys", + mock: func(ctrl *gomock.Controller) redis.Cmdable { + cmd := mocks.NewMockCmdable(ctrl) + status := redis.NewIntCmd(context.Background()) + status.SetVal(1) + status.SetErr(nil) + cmd.EXPECT(). + Del(context.Background(), gomock.Any(), gomock.Any(), gomock.Any()). + Return(status) + return cmd + }, + key: []string{"name", "age", "addr"}, + wantN: 1, + }, + { + name: "timeout", + mock: func(ctrl *gomock.Controller) redis.Cmdable { + cmd := mocks.NewMockCmdable(ctrl) + status := redis.NewIntCmd(context.Background()) + status.SetVal(0) + status.SetErr(context.DeadlineExceeded) + cmd.EXPECT(). + Del(context.Background(), gomock.Any()). + Return(status) + return cmd + }, + key: []string{"name"}, + wantErr: context.DeadlineExceeded, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + c := NewCache(tc.mock(ctrl)) + n, err := c.Delete(context.Background(), tc.key...) + assert.Equal(t, tc.wantN, n) + assert.Equal(t, tc.wantErr, err) + }) + } +} + func TestCache_LPush(t *testing.T) { testCase := []struct { name string diff --git a/script/integrate_test.sh b/script/integrate_test.sh index 3ee663c..492d134 100755 --- a/script/integrate_test.sh +++ b/script/integrate_test.sh @@ -16,8 +16,8 @@ set -e -docker-compose -f script/integration_test_compose.yml down +docker-compose -f script/integration_test_compose.yml down -v docker-compose -f script/integration_test_compose.yml up -d go test -race -cover ./... -tags=e2e -docker-compose -f script/integration_test_compose.yml down \ No newline at end of file +docker-compose -f script/integration_test_compose.yml down -v \ No newline at end of file diff --git a/types.go b/types.go index d5af6d4..6c8ab94 100644 --- a/types.go +++ b/types.go @@ -23,10 +23,14 @@ import ( "github.com/ecodeclub/ekit" ) +var ( + ErrKeyNeverExpireNotSupported = errors.New("不支持key永不过期") +) + type Cache interface { - // Set 设置一个键值对,并且设置过期时间 + // Set 设置一个键值对,并且设置过期时间,当过期时间为0时,表示永不过期 Set(ctx context.Context, key string, val any, expiration time.Duration) error - // SetNX 设置一个键值对如果key不存在则写入反之失败,并且设置过期时间 + // SetNX 设置一个键值对如果key不存在则写入反之失败,并且设置过期时间.当过期时间为0时,表示永不过期 SetNX(ctx context.Context, key string, val any, expiration time.Duration) (bool, error) // Get 返回一个 Value // 如果你需要检测 Err,可以使用 Value.Err @@ -34,6 +38,8 @@ type Cache interface { Get(ctx context.Context, key string) Value // GetSet 设置一个新的值返回老的值 如果key没有老的值仍然设置成功,但是返回 errs.ErrKeyNotExist GetSet(ctx context.Context, key string, val string) Value + // Delete 设置一个或多个键值对,当key不存在时,不计入删除数也不返回错误 + Delete(ctx context.Context, key ...string) (int64, error) // LPush 将所有指定值插入存储在 的列表的头部key。 // 如果key不存在,则在执行推送操作之前将其创建为空列表。当key保存的值不是列表时,将返回错误 // 默认返回列表的数量