From dda97435ac5c629e09ebc056bcccee00285d7aa6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=A5=BF=E6=9F=8A=E6=85=A7=E9=9F=B3?= Date: Fri, 13 Oct 2023 20:31:01 +0800 Subject: [PATCH] =?UTF-8?q?=E7=BA=A2=E9=BB=91=E6=A0=91+=E5=B0=8F=E6=A0=B9?= =?UTF-8?q?=E5=A0=86=E5=AE=9E=E7=8E=B0=E7=9A=84=E5=9F=BA=E4=BA=8E=E4=BC=98?= =?UTF-8?q?=E5=85=88=E7=BA=A7=E7=9A=84=E6=9C=AC=E5=9C=B0=E7=BC=93=E5=AD=98?= =?UTF-8?q?=20(#20)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 红黑树存储和小根堆优先级 * list 和 set 的实现 * 补充单元测试 * 补充单元测试 * 补充单元测试 * 补充单元测试 * 小根堆改成优先级队列 * ekit 改成 dev 分支 * 优先级方案改进 * 把和优先级有关的操作全部从缓存里抽出来 * 移除复杂的优先级支持逻辑(lru,lfu),只保留最基础的优先级逻辑 * 移除结点类型;所有结点参与淘汰; * 调整 setnx 的逻辑,移除无效的代码 * 处理 go mod 依赖问题 * 测试用例补全加锁逻辑 * 修改 ticker 的用法 --- go.mod | 3 +- go.sum | 6 +- memory/priority/priority.go | 21 + memory/priority/rbtree_cache_node.go | 119 ++ memory/priority/rbtree_priority_cache.go | 448 +++++ memory/priority/rbtree_priority_cache_test.go | 1593 +++++++++++++++++ 6 files changed, 2187 insertions(+), 3 deletions(-) create mode 100644 memory/priority/priority.go create mode 100644 memory/priority/rbtree_cache_node.go create mode 100644 memory/priority/rbtree_priority_cache.go create mode 100644 memory/priority/rbtree_priority_cache_test.go diff --git a/go.mod b/go.mod index a2a47c5..ad0b436 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/ecodeclub/ecache go 1.20 require ( - github.com/ecodeclub/ekit v0.0.8 + github.com/ecodeclub/ekit v0.0.8-0.20230925161647-c5bfbd460261 github.com/redis/go-redis/v9 v9.1.0 github.com/stretchr/testify v1.8.1 go.uber.org/mock v0.2.0 @@ -15,5 +15,6 @@ require ( 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 fb7d036..6300369 100644 --- a/go.sum +++ b/go.sum @@ -7,8 +7,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= -github.com/ecodeclub/ekit v0.0.8 h1:861Aot0GvD5ueREEYDVYc1oIhDuFyg6MTxIyiOa4Pvw= -github.com/ecodeclub/ekit v0.0.8/go.mod h1:OqTojKeKFTxeeAAUwNIPKu339SRkX6KAuoK/8A5BCEs= +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= @@ -26,6 +26,8 @@ github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKs github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= go.uber.org/mock v0.2.0 h1:TaP3xedm7JaAgScZO7tlvlKrqT0p7I6OsdGB5YNSMDU= go.uber.org/mock v0.2.0/go.mod h1:J0y0rp9L3xiff1+ZBfKxlC1fz2+aO16tw0tsDOixfuM= +golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/memory/priority/priority.go b/memory/priority/priority.go new file mode 100644 index 0000000..45be453 --- /dev/null +++ b/memory/priority/priority.go @@ -0,0 +1,21 @@ +// 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 priority + +// Priority 如果传进来的元素没有实现该接口,则默认优先级为0 +type Priority interface { + // GetPriority 获取元素的优先级 + GetPriority() int +} diff --git a/memory/priority/rbtree_cache_node.go b/memory/priority/rbtree_cache_node.go new file mode 100644 index 0000000..e181f95 --- /dev/null +++ b/memory/priority/rbtree_cache_node.go @@ -0,0 +1,119 @@ +// 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 priority + +import ( + "time" + + "github.com/ecodeclub/ekit/list" + "github.com/ecodeclub/ekit/set" + + "github.com/ecodeclub/ekit" +) + +// rbTreeCacheNode 缓存结点 +type rbTreeCacheNode struct { + key string //键 + value any //值 + deadline time.Time //有效期,默认0,永不过期 + priority int //优先级 + isDeleted bool //是否被删除 +} + +func newKVRBTreeCacheNode(key string, value any, expiration time.Duration) *rbTreeCacheNode { + node := &rbTreeCacheNode{ + key: key, + value: value, + } + node.setExpiration(expiration) + return node +} + +func newListRBTreeCacheNode(key string) *rbTreeCacheNode { + return &rbTreeCacheNode{ + key: key, + value: list.NewLinkedList[any](), + } +} + +func newSetRBTreeCacheNode(key string, initSize int) *rbTreeCacheNode { + return &rbTreeCacheNode{ + key: key, + value: set.NewMapSet[any](initSize), + } +} + +func newIntRBTreeCacheNode(key string) *rbTreeCacheNode { + return &rbTreeCacheNode{ + key: key, + value: int64(0), + } +} + +// setExpiration 设置有效期 +func (node *rbTreeCacheNode) setExpiration(expiration time.Duration) { + var deadline time.Time + if expiration != 0 { + deadline = time.Now().Add(expiration) + } + node.deadline = deadline +} + +// replace 重新设置缓存结点的value和有效期 +func (node *rbTreeCacheNode) replace(value any, expiration time.Duration) { + node.value = value + node.setExpiration(expiration) +} + +// beforeDeadline 检查传入的时间是不是在有效期之前 +func (node *rbTreeCacheNode) beforeDeadline(checkTime time.Time) bool { + if node.deadline.IsZero() { + return true + } + return checkTime.Before(node.deadline) +} + +// truncate 清空缓存结点中的数据 +func (node *rbTreeCacheNode) truncate() { + var nilValue any + node.value = nilValue + node.isDeleted = true +} + +// comparatorRBTreeCacheNodeByKey 缓存结点根据key的比较方式(给红黑树用) +func comparatorRBTreeCacheNodeByKey() ekit.Comparator[string] { + return func(src string, dst string) int { + if src < dst { + return -1 + } else if src == dst { + return 0 + } else { + return 1 + } + } +} + +// comparatorRBTreeCacheNodeByPriority 缓存结点根据优先级的比较方式(给优先级队列用) +func comparatorRBTreeCacheNodeByPriority() ekit.Comparator[*rbTreeCacheNode] { + return func(src *rbTreeCacheNode, dst *rbTreeCacheNode) int { + if src.priority < dst.priority { + return -1 + } else if src.priority == dst.priority { + return 0 + } else { + return 1 + } + } +} diff --git a/memory/priority/rbtree_priority_cache.go b/memory/priority/rbtree_priority_cache.go new file mode 100644 index 0000000..689a751 --- /dev/null +++ b/memory/priority/rbtree_priority_cache.go @@ -0,0 +1,448 @@ +// 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 priority + +import ( + "context" + "errors" + "math" + "sync" + "time" + + "github.com/ecodeclub/ekit/queue" + + "github.com/ecodeclub/ecache" + "github.com/ecodeclub/ecache/internal/errs" + "github.com/ecodeclub/ekit/bean/option" + "github.com/ecodeclub/ekit/list" + "github.com/ecodeclub/ekit/set" + "github.com/ecodeclub/ekit/tree" +) + +var ( + errOnlyListCanLPUSH = errors.New("ecache: 只有 list 类型的数据,才能执行 LPush") + errOnlyListCanLPOP = errors.New("ecache: 只有 list 类型的数据,才能执行 LPop") + errOnlySetCanSAdd = errors.New("ecache: 只有 set 类型的数据,才能执行 SAdd") + errOnlySetCanSRem = errors.New("ecache: 只有 set 类型的数据,才能执行 SRem") + errOnlyNumCanIncrBy = errors.New("ecache: 只有数字类型的数据,才能执行 IncrBy") + errOnlyNumCanDecrBy = errors.New("ecache: 只有数字类型的数据,才能执行 DecrBy") +) + +var ( + // todo 这两个变量没有想到更好办法处理,如果外部不调用option进行设置,最后还是需要内部定义一个默认值 + priorityQueueInitSize = 8 //优先级队列的初始大小 + mapSetInitSize = 8 //缓存结点中set.MapSet的初始大小 +) + +type RBTreePriorityCache struct { + globalLock *sync.RWMutex //内部全局读写锁,保护缓存数据和优先级数据 + cacheData *tree.RBTree[string, *rbTreeCacheNode] //缓存数据 + cacheNum int //缓存中总键值对数量 + cacheLimit int //键值对数量限制,默认MaxInt32,约等于没有限制 + priorityData *queue.PriorityQueue[*rbTreeCacheNode] //优先级数据 + defaultPriority int //默认优先级 +} + +func NewRBTreePriorityCache(opts ...option.Option[RBTreePriorityCache]) (*RBTreePriorityCache, error) { + cache, _ := newRBTreePriorityCache(opts...) + // todo 自动清理过期缓存的时间间隔,暂时先写死1s,后面再考虑怎么暴露出去 + go cache.autoClean(time.Second) + + return cache, nil +} + +func newRBTreePriorityCache(opts ...option.Option[RBTreePriorityCache]) (*RBTreePriorityCache, error) { + rbTree, _ := tree.NewRBTree[string, *rbTreeCacheNode](comparatorRBTreeCacheNodeByKey()) + priorityQueue := queue.NewPriorityQueue[*rbTreeCacheNode](priorityQueueInitSize, comparatorRBTreeCacheNodeByPriority()) + cache := &RBTreePriorityCache{ + globalLock: &sync.RWMutex{}, + cacheData: rbTree, + cacheNum: 0, + cacheLimit: math.MaxInt32, + priorityData: priorityQueue, + } + option.Apply(cache, opts...) + + return cache, nil +} + +func WithCacheLimit(cacheLimit int) option.Option[RBTreePriorityCache] { + return func(opt *RBTreePriorityCache) { + opt.cacheLimit = cacheLimit + } +} + +func WithDefaultPriority(priority int) option.Option[RBTreePriorityCache] { + return func(opt *RBTreePriorityCache) { + opt.defaultPriority = priority + } +} + +func (r *RBTreePriorityCache) Set(ctx context.Context, key string, val any, expiration time.Duration) error { + r.globalLock.Lock() + defer r.globalLock.Unlock() + + node, cacheErr := r.cacheData.Find(key) + if cacheErr != nil { + if r.isFull() { + r.deleteNodeByPriority() + } + node = newKVRBTreeCacheNode(key, val, expiration) + r.addNode(node) + + return nil + } + node.replace(val, expiration) + + return nil +} + +// addNode 把缓存结点添加到缓存结构中 +func (r *RBTreePriorityCache) addNode(node *rbTreeCacheNode) { + _ = r.cacheData.Add(node.key, node) //这里的error理论上不会出现 + r.cacheNum++ + r.addNodeToPriority(node) +} + +// deleteNode 把缓存结点从缓存结构中移除 +func (r *RBTreePriorityCache) deleteNode(node *rbTreeCacheNode) { + r.cacheData.Delete(node.key) + r.cacheNum-- + r.deleteNodeFromPriority(node) +} + +func (r *RBTreePriorityCache) SetNX(ctx context.Context, key string, val any, expiration time.Duration) (bool, error) { + r.globalLock.Lock() + defer r.globalLock.Unlock() + + node, cacheErr := r.cacheData.Find(key) + if cacheErr != nil { + node = newKVRBTreeCacheNode(key, val, expiration) + r.addNode(node) + + return true, nil + } + + if !node.beforeDeadline(time.Now()) { + node.replace(val, expiration) //过期的,key一样,直接覆盖 + + return true, nil + } + + return false, nil +} + +func (r *RBTreePriorityCache) Get(ctx context.Context, key string) (val ecache.Value) { + r.globalLock.RLock() + node, cacheErr := r.cacheData.Find(key) + r.globalLock.RUnlock() + + if cacheErr != nil { + val.Err = errs.ErrKeyNotExist + + return + } + + now := time.Now() + if !node.beforeDeadline(now) { + r.doubleCheckWhenExpire(node, now) + val.Err = errs.ErrKeyNotExist // 缓存过期归类为找不到 + + return + } + val.Val = node.value + + return +} + +// doubleCheckWhenExpire 缓存过期时的二次校验,防止被抢先删除了 +func (r *RBTreePriorityCache) doubleCheckWhenExpire(node *rbTreeCacheNode, now time.Time) { + r.globalLock.Lock() + defer r.globalLock.Unlock() + + checkNode, checkCacheErr := r.cacheData.Find(node.key) + if checkCacheErr != nil { + return //被抢先删除了 + } + if !checkNode.beforeDeadline(now) { + r.deleteNode(checkNode) + } +} + +func (r *RBTreePriorityCache) GetSet(ctx context.Context, key string, val string) ecache.Value { + r.globalLock.Lock() + defer r.globalLock.Unlock() + + var retVal ecache.Value + + node, cacheErr := r.cacheData.Find(key) + if cacheErr != nil { + retVal.Err = errs.ErrKeyNotExist + if r.isFull() { + r.deleteNodeByPriority() + } + node = newKVRBTreeCacheNode(key, val, 0) + r.addNode(node) + + return retVal + } + + //这里不需要判断缓存过期没有,取出旧值放入新值就完事了 + retVal.Val = node.value + node.value = val + + return retVal +} + +func (r *RBTreePriorityCache) LPush(ctx context.Context, key string, val ...any) (int64, error) { + r.globalLock.Lock() + defer r.globalLock.Unlock() + + node, cacheErr := r.cacheData.Find(key) + if cacheErr != nil { + if r.isFull() { + r.deleteNodeByPriority() + } + node = newListRBTreeCacheNode(key) + r.addNode(node) + } + + nodeVal, ok := node.value.(*list.LinkedList[any]) + if !ok { + return 0, errOnlyListCanLPUSH + } + + var successNum int64 + for item := range val { + _ = nodeVal.Add(0, item) //这里的error理论上是不会出现的 + successNum++ + } + + return successNum, nil +} + +func (r *RBTreePriorityCache) LPop(ctx context.Context, key string) ecache.Value { + r.globalLock.Lock() + defer r.globalLock.Unlock() + + var retVal ecache.Value + + node, cacheErr := r.cacheData.Find(key) + if cacheErr != nil { + retVal.Err = errs.ErrKeyNotExist + + return retVal + } + + nodeVal, ok := node.value.(*list.LinkedList[any]) + if !ok { + retVal.Err = errOnlyListCanLPOP + + return retVal + } + + retVal.Val, retVal.Err = nodeVal.Delete(0) //lpop就是删除并获取list的第一个元素 + + if nodeVal.Len() == 0 { + r.deleteNode(node) //如果列表为空就删除缓存结点 + } + + return retVal +} + +func (r *RBTreePriorityCache) SAdd(ctx context.Context, key string, members ...any) (int64, error) { + r.globalLock.Lock() + defer r.globalLock.Unlock() + + node, cacheErr := r.cacheData.Find(key) + if cacheErr != nil { + if r.isFull() { + r.deleteNodeByPriority() + } + node = newSetRBTreeCacheNode(key, mapSetInitSize) + r.addNode(node) + } + + nodeVal, ok := node.value.(*set.MapSet[any]) + if !ok { + return 0, errOnlySetCanSAdd + } + + var successNum int64 + for _, item := range members { + isExist := nodeVal.Exist(item) + if !isExist { + nodeVal.Add(item) + successNum++ + } + } + + return successNum, nil +} + +func (r *RBTreePriorityCache) SRem(ctx context.Context, key string, members ...any) ecache.Value { + r.globalLock.Lock() + defer r.globalLock.Unlock() + + var retVal ecache.Value + + node, cacheErr := r.cacheData.Find(key) + if cacheErr != nil { + retVal.Err = errs.ErrKeyNotExist + + return retVal + } + + nodeVal, ok := node.value.(*set.MapSet[any]) + if !ok { + retVal.Err = errOnlySetCanSRem + + return retVal + } + + successNum := 0 + for _, item := range members { + isExist := nodeVal.Exist(item) + if isExist { + nodeVal.Delete(item) + successNum++ + } + } + retVal.Val = int64(successNum) + + if len(nodeVal.Keys()) == 0 { + r.deleteNode(node) //如果集合为空,删除缓存结点 + } + + return retVal +} + +func (r *RBTreePriorityCache) IncrBy(ctx context.Context, key string, value int64) (int64, error) { + r.globalLock.Lock() + defer r.globalLock.Unlock() + + node, cacheErr := r.cacheData.Find(key) + if cacheErr != nil { + if r.isFull() { + r.deleteNodeByPriority() + } + node = newIntRBTreeCacheNode(key) + r.addNode(node) + } + + nodeVal, ok := node.value.(int64) + if !ok { + return 0, errOnlyNumCanIncrBy + } + + newVal := nodeVal + value + node.value = newVal + + return newVal, nil +} + +func (r *RBTreePriorityCache) DecrBy(ctx context.Context, key string, value int64) (int64, error) { + r.globalLock.Lock() + defer r.globalLock.Unlock() + + node, cacheErr := r.cacheData.Find(key) + if cacheErr != nil { + if r.isFull() { + r.deleteNodeByPriority() + } + node = newIntRBTreeCacheNode(key) + r.addNode(node) + } + + nodeVal, ok := node.value.(int64) + if !ok { + return 0, errOnlyNumCanDecrBy + } + + newVal := nodeVal - value + node.value = newVal + + return newVal, nil +} + +// calculatePriority 获取缓存数据的优先级权重 +func (r *RBTreePriorityCache) calculatePriority(node *rbTreeCacheNode) int { + priority := r.defaultPriority + + //如果实现了Priority接口,那么就用接口的方法获取优先级权重 + val, ok := node.value.(Priority) + if ok { + priority = val.GetPriority() + } + + return priority +} + +// addNodeToPriority 把缓存结点添加到优先级数据中去 +func (r *RBTreePriorityCache) addNodeToPriority(node *rbTreeCacheNode) { + node.priority = r.calculatePriority(node) + _ = r.priorityData.Enqueue(node) +} + +// deleteNodeFromPriority 从优先级数据中移除缓存结点 +func (r *RBTreePriorityCache) deleteNodeFromPriority(node *rbTreeCacheNode) { + //优先级队列无法随机删除结点 + //这里的方案是把优先级数据中的缓存结点置空,并标记为已删除 + //等到触发淘汰的时候再处理 + node.truncate() +} + +// isFull 键值对数量满了没有 +func (r *RBTreePriorityCache) isFull() bool { + return r.cacheNum >= r.cacheLimit +} + +// deleteNodeByPriority 根据优先级淘汰缓存结点【调用该方法必须先获得锁】 +func (r *RBTreePriorityCache) deleteNodeByPriority() { + for { + //这里需要循环,因为有的优先级结点是空的 + topNode, topErr := r.priorityData.Dequeue() + if topErr != nil { + return //走这里铁有bug,不可能缓存满了但是优先级队列是空的 + } + if topNode.isDeleted { + continue //空结点,直接回去,继续下一轮 + } + // 结点非空,删除缓存 + r.cacheData.Delete(topNode.key) + r.cacheNum-- + + return + } +} + +// autoClean 自动清理过期缓存 +func (r *RBTreePriorityCache) autoClean(interval time.Duration) { + ticker := time.NewTicker(interval) + defer ticker.Stop() + + for range ticker.C { + r.globalLock.RLock() + _, values := r.cacheData.KeyValues() + r.globalLock.RUnlock() + + now := time.Now() + for _, value := range values { + if !value.beforeDeadline(now) { + r.doubleCheckWhenExpire(value, now) + } + } + } +} diff --git a/memory/priority/rbtree_priority_cache_test.go b/memory/priority/rbtree_priority_cache_test.go new file mode 100644 index 0000000..98395ee --- /dev/null +++ b/memory/priority/rbtree_priority_cache_test.go @@ -0,0 +1,1593 @@ +// 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 priority + +import ( + "context" + "fmt" + "sync" + "testing" + "time" + + "github.com/ecodeclub/ecache/internal/errs" + "github.com/ecodeclub/ekit/list" + "github.com/ecodeclub/ekit/set" + "github.com/stretchr/testify/assert" +) + +// 测试用的,可以输入权重的结构 +type testStructForPriority struct { + priority int +} + +func (ts testStructForPriority) GetPriority() int { + return ts.priority +} + +func compareTwoRBTreeClient(src *RBTreePriorityCache, dst *RBTreePriorityCache) bool { + //如果缓存结构中的红黑树的大小一样,红黑树的每个key都有 + //键值对结点和数字结点中的元素一样,list和set结点中的元素数量一样 + //优先级队列长度一样,优先级队列顶部元素一样 + //那么就姑且认为两个缓存结构中的数据是一样的 + if src.cacheNum != dst.cacheNum { + return false + } + if src.cacheData.Size() != dst.cacheData.Size() { + return false + } + + src.globalLock.RLock() + srcKeys, srcNodes := src.cacheData.KeyValues() + src.globalLock.RUnlock() + srcKeysMap := make(map[string]*rbTreeCacheNode) + for index, item := range srcKeys { + srcKeysMap[item] = srcNodes[index] + } + dst.globalLock.RLock() + dstKeys, dstNodes := dst.cacheData.KeyValues() + dst.globalLock.RUnlock() + dstKeysMap := make(map[string]*rbTreeCacheNode) + for index, item := range dstKeys { + dstKeysMap[item] = dstNodes[index] + } + + for srcKey, srcNode := range srcKeysMap { + dstNode, ok := dstKeysMap[srcKey] + if !ok { + return false + } + + srcNodeVal1, ok1 := srcNode.value.(*list.LinkedList[any]) + if ok1 { + dstNodeVal11, ok11 := dstNode.value.(*list.LinkedList[any]) + if !ok11 { + return false + } + if srcNodeVal1.Len() != dstNodeVal11.Len() { + return false + } + continue + } + + srcNodeVal2, ok2 := srcNode.value.(*set.MapSet[any]) + if ok2 { + dstNodeVal22, ok22 := dstNode.value.(*set.MapSet[any]) + if !ok22 { + return false + } + if len(srcNodeVal2.Keys()) != len(dstNodeVal22.Keys()) { + return false + } + continue + } + + if srcNode.value != dstNode.value { + return false + } + } + + if src.priorityData.Len() != dst.priorityData.Len() { + return false + } + src.globalLock.Lock() + srcTop, _ := src.priorityData.Peek() + src.globalLock.Unlock() + dst.globalLock.Lock() + dstTop, _ := dst.priorityData.Peek() + dst.globalLock.Unlock() + if srcTop == nil && dstTop == nil { + return true + } + if (srcTop == nil && dstTop != nil) || (srcTop != nil && dstTop == nil) { + return false + } + if srcTop.key != dstTop.key { + return false + } + + return true +} + +func TestRBTreePriorityCache_Set(t *testing.T) { + testCases := []struct { + name string + startCache func() *RBTreePriorityCache + key string + value any + expiration time.Duration + wantCache func() *RBTreePriorityCache + wantErr error + }{ + { + name: "cache 0,add 1,ok", + startCache: func() *RBTreePriorityCache { + client, _ := NewRBTreePriorityCache() + return client + }, + key: "key1", + value: "value1", + wantCache: func() *RBTreePriorityCache { + cache, _ := NewRBTreePriorityCache() + cache.globalLock.Lock() + defer cache.globalLock.Unlock() + cache.addNode(newKVRBTreeCacheNode("key1", "value1", 0)) + return cache + }, + }, + { + name: "cache 1,add 1,ok", + startCache: func() *RBTreePriorityCache { + cache, _ := NewRBTreePriorityCache() + cache.globalLock.Lock() + defer cache.globalLock.Unlock() + cache.addNode(newKVRBTreeCacheNode("key1", "value1", 0)) + return cache + }, + key: "key2", + value: "value2", + wantCache: func() *RBTreePriorityCache { + cache, _ := NewRBTreePriorityCache() + cache.globalLock.Lock() + defer cache.globalLock.Unlock() + cache.addNode(newKVRBTreeCacheNode("key1", "value1", 0)) + cache.addNode(newKVRBTreeCacheNode("key2", "value2", 0)) + return cache + }, + }, + { + name: "cache 1,add 1,cover", + startCache: func() *RBTreePriorityCache { + cache, _ := NewRBTreePriorityCache() + cache.globalLock.Lock() + defer cache.globalLock.Unlock() + cache.addNode(newKVRBTreeCacheNode("key1", "value1", 0)) + return cache + }, + key: "key1", + value: "value2", + wantCache: func() *RBTreePriorityCache { + cache, _ := NewRBTreePriorityCache() + cache.globalLock.Lock() + defer cache.globalLock.Unlock() + cache.addNode(newKVRBTreeCacheNode("key1", "value2", 0)) + return cache + }, + }, + { + name: "limit 1,cache 1,add 1,evict", + startCache: func() *RBTreePriorityCache { + cache, _ := NewRBTreePriorityCache(WithCacheLimit(1)) + cache.globalLock.Lock() + defer cache.globalLock.Unlock() + cache.addNode(newKVRBTreeCacheNode("key1", "value1", 0)) + return cache + }, + key: "key2", + value: "value2", + wantCache: func() *RBTreePriorityCache { + cache, _ := NewRBTreePriorityCache() + cache.globalLock.Lock() + defer cache.globalLock.Unlock() + cache.addNode(newKVRBTreeCacheNode("key2", "value2", 0)) + return cache + }, + }, + { + name: "limit 2,cache 2,add 1,evict", + startCache: func() *RBTreePriorityCache { + cache, _ := NewRBTreePriorityCache(WithCacheLimit(2)) + cache.globalLock.Lock() + defer cache.globalLock.Unlock() + cache.addNode(newKVRBTreeCacheNode("key1", testStructForPriority{priority: 1}, 0)) + cache.addNode(newKVRBTreeCacheNode("key2", testStructForPriority{priority: 2}, 0)) + return cache + }, + key: "key3", + value: testStructForPriority{priority: 3}, + wantCache: func() *RBTreePriorityCache { + cache, _ := NewRBTreePriorityCache(WithCacheLimit(2)) + cache.globalLock.Lock() + defer cache.globalLock.Unlock() + cache.addNode(newKVRBTreeCacheNode("key2", testStructForPriority{priority: 2}, 0)) + cache.addNode(newKVRBTreeCacheNode("key3", testStructForPriority{priority: 3}, 0)) + return cache + }, + }, + { + name: "limit 2,cache 2,add 1,evict", + startCache: func() *RBTreePriorityCache { + cache, _ := NewRBTreePriorityCache(WithCacheLimit(2)) + cache.globalLock.Lock() + defer cache.globalLock.Unlock() + cache.addNode(newKVRBTreeCacheNode("key2", testStructForPriority{priority: 2}, 0)) + cache.addNode(newKVRBTreeCacheNode("key1", testStructForPriority{priority: 1}, 0)) + return cache + }, + key: "key3", + value: testStructForPriority{priority: 3}, + wantCache: func() *RBTreePriorityCache { + cache, _ := NewRBTreePriorityCache(WithCacheLimit(2)) + cache.globalLock.Lock() + defer cache.globalLock.Unlock() + cache.addNode(newKVRBTreeCacheNode("key2", testStructForPriority{priority: 2}, 0)) + cache.addNode(newKVRBTreeCacheNode("key3", testStructForPriority{priority: 3}, 0)) + return cache + }, + }, + { + name: "limit 2,cache 2,add 1,evict", + startCache: func() *RBTreePriorityCache { + cache, _ := NewRBTreePriorityCache(WithCacheLimit(2), WithDefaultPriority(5)) + cache.globalLock.Lock() + defer cache.globalLock.Unlock() + cache.addNode(newKVRBTreeCacheNode("key1", "value1", 0)) + cache.addNode(newKVRBTreeCacheNode("key2", testStructForPriority{priority: 2}, 0)) + return cache + }, + key: "key3", + value: testStructForPriority{priority: 3}, + wantCache: func() *RBTreePriorityCache { + cache, _ := NewRBTreePriorityCache(WithCacheLimit(2), WithDefaultPriority(5)) + cache.globalLock.Lock() + defer cache.globalLock.Unlock() + cache.addNode(newKVRBTreeCacheNode("key1", "value1", 0)) + cache.addNode(newKVRBTreeCacheNode("key3", testStructForPriority{priority: 3}, 0)) + return cache + }, + }, + { + name: "limit 1,cache 1,add 1,evict,cover empty priority queue top", + startCache: func() *RBTreePriorityCache { + cache, _ := NewRBTreePriorityCache(WithCacheLimit(1)) + cache.globalLock.Lock() + defer cache.globalLock.Unlock() + node1 := newKVRBTreeCacheNode("key1", testStructForPriority{priority: 1}, 0) + cache.addNode(node1) + cache.deleteNode(node1) //模拟删除结点,构造空的优先级队列头 + cache.addNode(newKVRBTreeCacheNode("key2", testStructForPriority{priority: 2}, 0)) + return cache + }, + key: "key3", + value: testStructForPriority{priority: 3}, + wantCache: func() *RBTreePriorityCache { + cache, _ := NewRBTreePriorityCache(WithCacheLimit(1)) + cache.globalLock.Lock() + defer cache.globalLock.Unlock() + cache.addNode(newKVRBTreeCacheNode("key3", testStructForPriority{priority: 3}, 0)) + return cache + }, + }, + { + name: "limit 1,cache 1,add 1,evict,cover heap top nil,should not happen", + startCache: func() *RBTreePriorityCache { + cache, _ := NewRBTreePriorityCache(WithCacheLimit(1)) + cache.globalLock.Lock() + defer cache.globalLock.Unlock() + node1 := newKVRBTreeCacheNode("key1", "value1", 0) + _ = cache.cacheData.Add("key1", node1) + cache.cacheNum++ + //这里不应该出现没有设置的情况,出现这种这种情况肯定有bug + //cache.SetCacheNodePriority(node1) + + return cache + }, + key: "key2", + value: "value2", + wantCache: func() *RBTreePriorityCache { + cache, _ := NewRBTreePriorityCache(WithCacheLimit(1)) + cache.globalLock.Lock() + defer cache.globalLock.Unlock() + //上面的bug导致这个结点没被删掉 + node1 := newKVRBTreeCacheNode("key1", "value1", 0) + _ = cache.cacheData.Add("key1", node1) + cache.cacheNum++ + cache.addNode(newKVRBTreeCacheNode("key2", "value2", 0)) + return cache + }, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + startCache := tc.startCache() + err := startCache.Set(context.Background(), tc.key, tc.value, tc.expiration) + assert.Equal(t, tc.wantErr, err) + if err != nil { + return + } + assert.Equal(t, true, compareTwoRBTreeClient(startCache, tc.wantCache())) + }) + } +} + +func TestCache_Set2(t *testing.T) { + cacheLimit := 100 + cache, _ := NewRBTreePriorityCache(WithCacheLimit(cacheLimit)) + key := "key" + value := "value" + + wg := sync.WaitGroup{} + for i := 1; i <= 10000; i++ { + wg.Add(1) + j := i + go func() { + tempKey := fmt.Sprintf("%s%d", key, j) + tempValue := fmt.Sprintf("%s%d", value, j) + _ = cache.Set(context.Background(), tempKey, tempValue, 0) + wg.Done() + }() + } + wg.Wait() + + assert.Equal(t, cacheLimit, cache.cacheNum) +} + +func TestRBTreePriorityCache_SetNX(t *testing.T) { + testCases := []struct { + name string + startCache func() *RBTreePriorityCache + key string + value any + expiration time.Duration + wantCache func() *RBTreePriorityCache + wantBool bool + wantErr error + }{ + { + name: "cache 0,add 1,ok", + startCache: func() *RBTreePriorityCache { + cache, _ := NewRBTreePriorityCache() + return cache + }, + key: "key1", + value: "value1", + wantCache: func() *RBTreePriorityCache { + cache, _ := NewRBTreePriorityCache() + cache.globalLock.Lock() + defer cache.globalLock.Unlock() + cache.addNode(newKVRBTreeCacheNode("key1", "value1", 0)) + return cache + }, + wantBool: true, + }, + { + name: "cache 0,add 1,not conflict", + startCache: func() *RBTreePriorityCache { + cache, _ := NewRBTreePriorityCache() + cache.globalLock.Lock() + defer cache.globalLock.Unlock() + cache.addNode(newKVRBTreeCacheNode("key1", "value1", 0)) + return cache + }, + key: "key2", + value: "value2", + wantCache: func() *RBTreePriorityCache { + cache, _ := NewRBTreePriorityCache() + cache.globalLock.Lock() + defer cache.globalLock.Unlock() + cache.addNode(newKVRBTreeCacheNode("key1", "value1", 0)) + cache.addNode(newKVRBTreeCacheNode("key2", "value2", 0)) + return cache + }, + wantBool: true, + }, + { + name: "cache 1,add 1,conflict,self", + startCache: func() *RBTreePriorityCache { + cache, _ := NewRBTreePriorityCache() + cache.globalLock.Lock() + defer cache.globalLock.Unlock() + cache.addNode(newKVRBTreeCacheNode("key1", "value1", 0)) + return cache + }, + key: "key1", + value: "value1", + wantCache: func() *RBTreePriorityCache { + cache, _ := NewRBTreePriorityCache() + cache.globalLock.Lock() + defer cache.globalLock.Unlock() + cache.addNode(newKVRBTreeCacheNode("key1", "value1", 0)) + return cache + }, + wantBool: false, + }, + { + name: "cache 1,add 1,conflict,failed", + startCache: func() *RBTreePriorityCache { + cache, _ := NewRBTreePriorityCache() + cache.globalLock.Lock() + defer cache.globalLock.Unlock() + cache.addNode(newKVRBTreeCacheNode("key1", "value1", 0)) + return cache + }, + key: "key1", + value: "value2", + wantCache: func() *RBTreePriorityCache { + cache, _ := NewRBTreePriorityCache() + cache.globalLock.Lock() + defer cache.globalLock.Unlock() + cache.addNode(newKVRBTreeCacheNode("key1", "value1", 0)) + return cache + }, + wantBool: false, + }, + { + name: "cache 1,add 1,conflict,expired", + startCache: func() *RBTreePriorityCache { + cache, _ := NewRBTreePriorityCache() + cache.globalLock.Lock() + defer cache.globalLock.Unlock() + cache.addNode(newKVRBTreeCacheNode("key1", "value1", -time.Minute)) + return cache + }, + key: "key1", + value: "value2", + wantCache: func() *RBTreePriorityCache { + cache, _ := NewRBTreePriorityCache() + cache.globalLock.Lock() + defer cache.globalLock.Unlock() + cache.addNode(newKVRBTreeCacheNode("key1", "value2", 0)) + return cache + }, + wantBool: true, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + startCache := tc.startCache() + retBool, err := startCache.SetNX(context.Background(), tc.key, tc.value, tc.expiration) + assert.Equal(t, tc.wantErr, err) + if err != nil { + return + } + assert.Equal(t, tc.wantBool, retBool) + }) + } +} + +func TestRBTreePriorityCache_Get(t *testing.T) { + testCases := []struct { + name string + startCache func() *RBTreePriorityCache + key string + wantCache func() *RBTreePriorityCache + wantValue any + wantErr error + }{ + { + name: "cache 0,miss", + startCache: func() *RBTreePriorityCache { + cache, _ := NewRBTreePriorityCache() + return cache + }, + key: "key1", + wantCache: func() *RBTreePriorityCache { + cache, _ := NewRBTreePriorityCache() + return cache + }, + wantErr: errs.ErrKeyNotExist, + }, + { + name: "cache 1,miss", + startCache: func() *RBTreePriorityCache { + cache, _ := NewRBTreePriorityCache() + cache.globalLock.Lock() + defer cache.globalLock.Unlock() + cache.addNode(newKVRBTreeCacheNode("key1", "value1", 0)) + return cache + }, + key: "key2", + wantCache: func() *RBTreePriorityCache { + cache, _ := NewRBTreePriorityCache() + cache.globalLock.Lock() + defer cache.globalLock.Unlock() + cache.addNode(newKVRBTreeCacheNode("key1", "value1", 0)) + return cache + }, + wantErr: errs.ErrKeyNotExist, + }, + { + name: "cache 1,hit", + startCache: func() *RBTreePriorityCache { + cache, _ := NewRBTreePriorityCache() + cache.globalLock.Lock() + defer cache.globalLock.Unlock() + cache.addNode(newKVRBTreeCacheNode("key1", "value1", 0)) + return cache + }, + key: "key1", + wantCache: func() *RBTreePriorityCache { + cache, _ := NewRBTreePriorityCache() + cache.globalLock.Lock() + defer cache.globalLock.Unlock() + cache.addNode(newKVRBTreeCacheNode("key1", "value1", 0)) + return cache + }, + wantValue: "value1", + }, + { + name: "cache num 1,hit,expire", + startCache: func() *RBTreePriorityCache { + cache, _ := NewRBTreePriorityCache() + cache.globalLock.Lock() + defer cache.globalLock.Unlock() + cache.addNode(newKVRBTreeCacheNode("key1", "value1", -time.Minute)) + return cache + }, + key: "key1", + wantCache: func() *RBTreePriorityCache { + client, _ := NewRBTreePriorityCache() + return client + }, + wantErr: errs.ErrKeyNotExist, + }, + { + name: "cache 1,hit,not expire", + startCache: func() *RBTreePriorityCache { + cache, _ := NewRBTreePriorityCache() + cache.globalLock.Lock() + defer cache.globalLock.Unlock() + cache.addNode(newKVRBTreeCacheNode("key1", "value1", 0)) + return cache + }, + key: "key1", + wantCache: func() *RBTreePriorityCache { + cache, _ := NewRBTreePriorityCache() + cache.addNode(newKVRBTreeCacheNode("key1", "value1", 0)) + return cache + }, + wantValue: "value1", + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + startCache := tc.startCache() + value := startCache.Get(context.Background(), tc.key) + assert.Equal(t, tc.wantErr, value.Err) + if value.Err != nil { + return + } + assert.Equal(t, tc.wantValue, value.Val) + }) + } +} + +func TestRBTreePriorityCache_doubleCheckInGet(t *testing.T) { + testCases := []struct { + name string + startCache func() *RBTreePriorityCache + node *rbTreeCacheNode + wantCache func() *RBTreePriorityCache + }{ + { + name: "key not deleted by other thread", + startCache: func() *RBTreePriorityCache { + cache, _ := NewRBTreePriorityCache() + cache.globalLock.Lock() + defer cache.globalLock.Unlock() + cache.addNode(newKVRBTreeCacheNode("key1", "value1", -time.Minute)) + return cache + }, + node: newKVRBTreeCacheNode("key1", "value1", -time.Minute), + wantCache: func() *RBTreePriorityCache { + cache, _ := NewRBTreePriorityCache() + cache.globalLock.Lock() + defer cache.globalLock.Unlock() + node1 := newKVRBTreeCacheNode("key1", "value1", -time.Minute) + cache.addNode(node1) + cache.deleteNode(node1) + return cache + }, + }, + { + name: "key deleted by other thread", + startCache: func() *RBTreePriorityCache { + cache, _ := NewRBTreePriorityCache() + return cache + }, + node: newKVRBTreeCacheNode("key1", "value1", -time.Minute), + wantCache: func() *RBTreePriorityCache { + cache, _ := NewRBTreePriorityCache() + return cache + }, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + startCache := tc.startCache() + startCache.doubleCheckWhenExpire(tc.node, time.Now()) + assert.Equal(t, true, compareTwoRBTreeClient(startCache, tc.wantCache())) + }) + } +} + +func TestRBTreePriorityCache_GetSet(t *testing.T) { + testCases := []struct { + name string + startCache func() *RBTreePriorityCache + key string + value string + wantCache func() *RBTreePriorityCache + wantValue any + wantErr error + }{ + { + name: "cache 0,miss,add 1", + startCache: func() *RBTreePriorityCache { + cache, _ := NewRBTreePriorityCache() + return cache + }, + key: "key1", + value: "value1", + wantCache: func() *RBTreePriorityCache { + cache, _ := NewRBTreePriorityCache() + cache.globalLock.Lock() + defer cache.globalLock.Unlock() + cache.addNode(newKVRBTreeCacheNode("key1", "value1", 0)) + return cache + }, + wantErr: errs.ErrKeyNotExist, + }, + { + name: "cache 1,miss,add 1", + startCache: func() *RBTreePriorityCache { + cache, _ := NewRBTreePriorityCache() + cache.globalLock.Lock() + defer cache.globalLock.Unlock() + cache.addNode(newKVRBTreeCacheNode("key1", "value1", 0)) + cache.cacheNum++ + return cache + }, + key: "key2", + value: "value2", + wantCache: func() *RBTreePriorityCache { + cache, _ := NewRBTreePriorityCache() + cache.globalLock.Lock() + defer cache.globalLock.Unlock() + cache.addNode(newKVRBTreeCacheNode("key1", "value1", 0)) + cache.addNode(newKVRBTreeCacheNode("key2", "value2", 0)) + return cache + }, + wantErr: errs.ErrKeyNotExist, + }, + { + name: "cache 1,hit", + startCache: func() *RBTreePriorityCache { + cache, _ := NewRBTreePriorityCache() + cache.globalLock.Lock() + defer cache.globalLock.Unlock() + cache.addNode(newKVRBTreeCacheNode("key1", "value1", 0)) + return cache + }, + key: "key1", + value: "value2", + wantCache: func() *RBTreePriorityCache { + cache, _ := NewRBTreePriorityCache() + cache.globalLock.Lock() + defer cache.globalLock.Unlock() + cache.addNode(newKVRBTreeCacheNode("key1", "value2", 0)) + return cache + }, + wantValue: "value1", + }, + { + name: "cache 1,hit,expired", + startCache: func() *RBTreePriorityCache { + cache, _ := NewRBTreePriorityCache() + cache.globalLock.Lock() + defer cache.globalLock.Unlock() + cache.addNode(newKVRBTreeCacheNode("key1", "value1", -time.Minute)) + return cache + }, + key: "key1", + value: "value2", + wantCache: func() *RBTreePriorityCache { + cache, _ := NewRBTreePriorityCache() + cache.globalLock.Lock() + defer cache.globalLock.Unlock() + cache.addNode(newKVRBTreeCacheNode("key1", "value2", 0)) + return cache + }, + wantValue: "value1", + }, + { + name: "limit 1,cache 1,miss,add 1,evict", + startCache: func() *RBTreePriorityCache { + cache, _ := NewRBTreePriorityCache(WithCacheLimit(1)) + cache.globalLock.Lock() + defer cache.globalLock.Unlock() + cache.addNode(newKVRBTreeCacheNode("key1", "value1", 0)) + return cache + }, + key: "key2", + value: "value2", + wantCache: func() *RBTreePriorityCache { + cache, _ := NewRBTreePriorityCache() + cache.globalLock.Lock() + defer cache.globalLock.Unlock() + cache.addNode(newKVRBTreeCacheNode("key2", "value2", 0)) + return cache + }, + wantErr: errs.ErrKeyNotExist, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + startCache := tc.startCache() + value := startCache.GetSet(context.Background(), tc.key, tc.value) + assert.Equal(t, tc.wantErr, value.Err) + if value.Err != nil { + return + } + assert.Equal(t, tc.wantValue, value.Val) + assert.Equal(t, true, compareTwoRBTreeClient(startCache, tc.wantCache())) + }) + } +} + +func TestRBTreePriorityCache_LPush(t *testing.T) { + testCases := []struct { + name string + startCache func() *RBTreePriorityCache + key string + value []any + wantCache func() *RBTreePriorityCache + wantNum int64 + wantErr error + }{ + { + name: "cache 0,push 1", + startCache: func() *RBTreePriorityCache { + cache, _ := NewRBTreePriorityCache() + return cache + }, + key: "key1", + value: []any{"value1"}, + wantCache: func() *RBTreePriorityCache { + cache, _ := NewRBTreePriorityCache() + cache.globalLock.Lock() + defer cache.globalLock.Unlock() + valList := list.NewLinkedList[any]() + _ = valList.Append("value1") + node1 := newListRBTreeCacheNode("key1") + node1.value = valList + cache.addNode(node1) + return cache + }, + wantNum: 1, + }, + { + name: "cache 1,item 1,push 1", + startCache: func() *RBTreePriorityCache { + cache, _ := NewRBTreePriorityCache() + cache.globalLock.Lock() + defer cache.globalLock.Unlock() + valList := list.NewLinkedList[any]() + _ = valList.Append("value1") + node1 := newListRBTreeCacheNode("key1") + node1.value = valList + cache.addNode(node1) + return cache + }, + key: "key1", + value: []any{"value2"}, + wantCache: func() *RBTreePriorityCache { + cache, _ := NewRBTreePriorityCache() + cache.globalLock.Lock() + defer cache.globalLock.Unlock() + valList := list.NewLinkedList[any]() + _ = valList.Append("value1") + _ = valList.Append("value2") + node1 := newListRBTreeCacheNode("key1") + node1.value = valList + cache.addNode(node1) + return cache + }, + wantNum: 1, + }, + { + name: "cache 0,push 2", + startCache: func() *RBTreePriorityCache { + cache, _ := NewRBTreePriorityCache() + return cache + }, + key: "key1", + value: []any{"value1", "value2"}, + wantCache: func() *RBTreePriorityCache { + cache, _ := NewRBTreePriorityCache() + cache.globalLock.Lock() + defer cache.globalLock.Unlock() + valList := list.NewLinkedList[any]() + _ = valList.Append("value1") + _ = valList.Append("value2") + node1 := newListRBTreeCacheNode("key1") + node1.value = valList + cache.addNode(node1) + return cache + }, + wantNum: 2, + }, + { + name: "limit 1,cache 1,push 1,evict", + startCache: func() *RBTreePriorityCache { + cache, _ := NewRBTreePriorityCache(WithCacheLimit(1)) + cache.globalLock.Lock() + defer cache.globalLock.Unlock() + valList := list.NewLinkedList[any]() + _ = valList.Append("value1") + node1 := newListRBTreeCacheNode("key1") + node1.value = valList + cache.addNode(node1) + return cache + }, + key: "key2", + value: []any{"value2"}, + wantCache: func() *RBTreePriorityCache { + cache, _ := NewRBTreePriorityCache(WithCacheLimit(1)) + cache.globalLock.Lock() + defer cache.globalLock.Unlock() + valList := list.NewLinkedList[any]() + _ = valList.Append("value2") + node1 := newListRBTreeCacheNode("key2") + node1.value = valList + cache.addNode(node1) + return cache + }, + wantNum: 1, + }, + { + name: "wrong type", + startCache: func() *RBTreePriorityCache { + cache, _ := NewRBTreePriorityCache() + cache.globalLock.Lock() + defer cache.globalLock.Unlock() + cache.addNode(newKVRBTreeCacheNode("key1", "value1", 0)) + return cache + }, + key: "key1", + wantCache: func() *RBTreePriorityCache { + cache, _ := NewRBTreePriorityCache() + cache.globalLock.Lock() + defer cache.globalLock.Unlock() + cache.addNode(newKVRBTreeCacheNode("key1", "value1", 0)) + return cache + }, + wantErr: errOnlyListCanLPUSH, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + startCache := tc.startCache() + num, err := startCache.LPush(context.Background(), tc.key, tc.value...) + assert.Equal(t, tc.wantErr, err) + if err != nil { + return + } + assert.Equal(t, tc.wantNum, num) + assert.Equal(t, true, compareTwoRBTreeClient(startCache, tc.wantCache())) + }) + } +} + +func TestRBTreePriorityCache_LPop(t *testing.T) { + testCases := []struct { + name string + startCache func() *RBTreePriorityCache + key string + wantCache func() *RBTreePriorityCache + wantValue any + wantErr error + }{ + { + name: "cache 0,miss", + startCache: func() *RBTreePriorityCache { + cache, _ := NewRBTreePriorityCache() + return cache + }, + key: "key1", + wantCache: func() *RBTreePriorityCache { + cache, _ := NewRBTreePriorityCache() + return cache + }, + wantErr: errs.ErrKeyNotExist, + }, + { + name: "cache 1,item 1,pop 1,delete node", + startCache: func() *RBTreePriorityCache { + cache, _ := NewRBTreePriorityCache() + cache.globalLock.Lock() + defer cache.globalLock.Unlock() + valList := list.NewLinkedList[any]() + _ = valList.Append("value1") + node1 := newListRBTreeCacheNode("key1") + node1.value = valList + cache.addNode(node1) + return cache + }, + key: "key1", + wantCache: func() *RBTreePriorityCache { + cache, _ := NewRBTreePriorityCache() + cache.globalLock.Lock() + defer cache.globalLock.Unlock() + node1 := newListRBTreeCacheNode("key1") + cache.addNode(node1) + cache.deleteNode(node1) + return cache + }, + wantValue: "value1", + }, + { + name: "cache 1,item 2,pop 1", + startCache: func() *RBTreePriorityCache { + cache, _ := NewRBTreePriorityCache() + cache.globalLock.Lock() + defer cache.globalLock.Unlock() + valList := list.NewLinkedList[any]() + _ = valList.Append("value1") + _ = valList.Append("value2") + node1 := newListRBTreeCacheNode("key1") + node1.value = valList + cache.addNode(node1) + return cache + }, + key: "key1", + wantCache: func() *RBTreePriorityCache { + cache, _ := NewRBTreePriorityCache() + cache.globalLock.Lock() + defer cache.globalLock.Unlock() + valList := list.NewLinkedList[any]() + _ = valList.Append("value1") + node1 := newListRBTreeCacheNode("key1") + node1.value = valList + cache.addNode(node1) + return cache + }, + wantValue: "value1", + }, + { + name: "wrong type", + startCache: func() *RBTreePriorityCache { + cache, _ := NewRBTreePriorityCache() + cache.globalLock.Lock() + defer cache.globalLock.Unlock() + cache.addNode(newKVRBTreeCacheNode("key1", "value1", 0)) + return cache + }, + key: "key1", + wantCache: func() *RBTreePriorityCache { + cache, _ := NewRBTreePriorityCache() + cache.globalLock.Lock() + defer cache.globalLock.Unlock() + cache.addNode(newKVRBTreeCacheNode("key1", "value1", 0)) + return cache + }, + wantErr: errOnlyListCanLPOP, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + startCache := tc.startCache() + value := startCache.LPop(context.Background(), tc.key) + assert.Equal(t, tc.wantErr, value.Err) + if value.Err != nil { + return + } + assert.Equal(t, tc.wantValue, value.Val) + assert.Equal(t, true, compareTwoRBTreeClient(startCache, tc.wantCache())) + }) + } +} + +func TestRBTreePriorityCache_SAdd(t *testing.T) { + testCases := []struct { + name string + startCache func() *RBTreePriorityCache + key string + values []any + wantCache func() *RBTreePriorityCache + wantRet int64 + wantErr error + }{ + { + name: "cache 0,add 1", + startCache: func() *RBTreePriorityCache { + client, _ := NewRBTreePriorityCache() + return client + }, + key: "key1", + values: []any{"value1"}, + wantCache: func() *RBTreePriorityCache { + cache, _ := NewRBTreePriorityCache() + cache.globalLock.Lock() + defer cache.globalLock.Unlock() + valSet1 := set.NewMapSet[any](mapSetInitSize) + valSet1.Add("value1") + node1 := newSetRBTreeCacheNode("key1", mapSetInitSize) + node1.value = valSet1 + cache.addNode(node1) + return cache + }, + wantRet: 1, + }, + { + name: "cache 1,add 1,not repeat", + startCache: func() *RBTreePriorityCache { + cache, _ := NewRBTreePriorityCache() + cache.globalLock.Lock() + defer cache.globalLock.Unlock() + valSet1 := set.NewMapSet[any](mapSetInitSize) + valSet1.Add("value1") + node1 := newSetRBTreeCacheNode("key1", mapSetInitSize) + node1.value = valSet1 + cache.addNode(node1) + return cache + }, + key: "key1", + values: []any{"value2"}, + wantCache: func() *RBTreePriorityCache { + cache, _ := NewRBTreePriorityCache() + cache.globalLock.Lock() + defer cache.globalLock.Unlock() + valSet1 := set.NewMapSet[any](mapSetInitSize) + valSet1.Add("value1") + valSet1.Add("value2") + node1 := newSetRBTreeCacheNode("key1", mapSetInitSize) + node1.value = valSet1 + cache.addNode(node1) + return cache + }, + wantRet: 1, + }, + { + name: "cache 1,add 1,repeat", + startCache: func() *RBTreePriorityCache { + cache, _ := NewRBTreePriorityCache() + cache.globalLock.Lock() + defer cache.globalLock.Unlock() + valSet1 := set.NewMapSet[any](mapSetInitSize) + valSet1.Add("value1") + node1 := newSetRBTreeCacheNode("key1", mapSetInitSize) + node1.value = valSet1 + cache.addNode(node1) + return cache + }, + key: "key1", + values: []any{"value1"}, + wantCache: func() *RBTreePriorityCache { + cache, _ := NewRBTreePriorityCache() + cache.globalLock.Lock() + defer cache.globalLock.Unlock() + valSet1 := set.NewMapSet[any](mapSetInitSize) + valSet1.Add("value1") + node1 := newSetRBTreeCacheNode("key1", mapSetInitSize) + node1.value = valSet1 + cache.addNode(node1) + return cache + }, + wantRet: 0, + }, + { + name: "cache 0,add 2", + startCache: func() *RBTreePriorityCache { + cache, _ := NewRBTreePriorityCache() + return cache + }, + key: "key1", + values: []any{"value1", "value2"}, + wantCache: func() *RBTreePriorityCache { + cache, _ := NewRBTreePriorityCache() + cache.globalLock.Lock() + defer cache.globalLock.Unlock() + valSet1 := set.NewMapSet[any](mapSetInitSize) + valSet1.Add("value1") + valSet1.Add("value2") + node1 := newSetRBTreeCacheNode("key1", mapSetInitSize) + node1.value = valSet1 + cache.addNode(node1) + return cache + }, + wantRet: 2, + }, + { + name: "limit 1,cache 1,add 1,evict", + startCache: func() *RBTreePriorityCache { + cache, _ := NewRBTreePriorityCache(WithCacheLimit(1)) + cache.globalLock.Lock() + defer cache.globalLock.Unlock() + valSet1 := set.NewMapSet[any](mapSetInitSize) + valSet1.Add("value1") + node1 := newSetRBTreeCacheNode("key1", mapSetInitSize) + node1.value = valSet1 + cache.addNode(node1) + return cache + }, + key: "key2", + values: []any{"value2"}, + wantCache: func() *RBTreePriorityCache { + cache, _ := NewRBTreePriorityCache(WithCacheLimit(1)) + cache.globalLock.Lock() + defer cache.globalLock.Unlock() + valSet1 := set.NewMapSet[any](mapSetInitSize) + valSet1.Add("value2") + node1 := newSetRBTreeCacheNode("key2", mapSetInitSize) + node1.value = valSet1 + cache.addNode(node1) + return cache + }, + wantRet: 1, + }, + { + name: "wrong type", + startCache: func() *RBTreePriorityCache { + cache, _ := NewRBTreePriorityCache() + cache.globalLock.Lock() + defer cache.globalLock.Unlock() + cache.addNode(newKVRBTreeCacheNode("key1", "value1", 0)) + return cache + }, + key: "key1", + values: []any{"value1"}, + wantCache: func() *RBTreePriorityCache { + cache, _ := NewRBTreePriorityCache() + cache.globalLock.Lock() + defer cache.globalLock.Unlock() + cache.addNode(newKVRBTreeCacheNode("key1", "value1", 0)) + return cache + }, + wantRet: 0, + wantErr: errOnlySetCanSAdd, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + startCache := tc.startCache() + ret, err := startCache.SAdd(context.Background(), tc.key, tc.values...) + assert.Equal(t, tc.wantErr, err) + if err != nil { + return + } + assert.Equal(t, tc.wantRet, ret) + assert.Equal(t, true, compareTwoRBTreeClient(startCache, tc.wantCache())) + }) + } +} + +func TestRBTreePriorityCache_SRem(t *testing.T) { + testCases := []struct { + name string + startCache func() *RBTreePriorityCache + key string + values []any + wantCache func() *RBTreePriorityCache + wantRet int64 + wantErr error + }{ + { + name: "cache 0,rem 1,miss", + startCache: func() *RBTreePriorityCache { + client, _ := NewRBTreePriorityCache() + return client + }, + key: "key1", + values: []any{"value1"}, + wantCache: func() *RBTreePriorityCache { + client, _ := NewRBTreePriorityCache() + return client + }, + wantRet: 0, + wantErr: errs.ErrKeyNotExist, + }, + { + name: "cache 1,item 1,rem 1,hit,delete node", + startCache: func() *RBTreePriorityCache { + cache, _ := NewRBTreePriorityCache() + cache.globalLock.Lock() + defer cache.globalLock.Unlock() + valSet1 := set.NewMapSet[any](mapSetInitSize) + valSet1.Add("value1") + node1 := newSetRBTreeCacheNode("key1", mapSetInitSize) + node1.value = valSet1 + cache.addNode(node1) + return cache + }, + key: "key1", + values: []any{"value1"}, + wantCache: func() *RBTreePriorityCache { + cache, _ := NewRBTreePriorityCache() + cache.globalLock.Lock() + defer cache.globalLock.Unlock() + valSet1 := set.NewMapSet[any](mapSetInitSize) + valSet1.Add("value1") + node1 := newSetRBTreeCacheNode("key1", mapSetInitSize) + node1.value = valSet1 + cache.addNode(node1) + cache.deleteNode(node1) + return cache + }, + wantRet: 1, + }, + { + name: "cache 1,item 1,rem 1,miss", + startCache: func() *RBTreePriorityCache { + cache, _ := NewRBTreePriorityCache() + cache.globalLock.Lock() + defer cache.globalLock.Unlock() + valSet1 := set.NewMapSet[any](mapSetInitSize) + valSet1.Add("value1") + node1 := newSetRBTreeCacheNode("key1", mapSetInitSize) + node1.value = valSet1 + cache.addNode(node1) + return cache + }, + key: "key1", + values: []any{"value2"}, + wantCache: func() *RBTreePriorityCache { + cache, _ := NewRBTreePriorityCache() + cache.globalLock.Lock() + defer cache.globalLock.Unlock() + valSet1 := set.NewMapSet[any](mapSetInitSize) + valSet1.Add("value1") + node1 := newSetRBTreeCacheNode("key1", mapSetInitSize) + node1.value = valSet1 + cache.addNode(node1) + return cache + }, + wantRet: 0, + }, + { + name: "cache 1,item 2,rem 2,hit 2", + startCache: func() *RBTreePriorityCache { + cache, _ := NewRBTreePriorityCache() + cache.globalLock.Lock() + defer cache.globalLock.Unlock() + valSet1 := set.NewMapSet[any](mapSetInitSize) + valSet1.Add("value1") + valSet1.Add("value2") + node1 := newSetRBTreeCacheNode("key1", mapSetInitSize) + node1.value = valSet1 + cache.addNode(node1) + return cache + }, + key: "key1", + values: []any{"value1", "value2"}, + wantCache: func() *RBTreePriorityCache { + cache, _ := NewRBTreePriorityCache() + cache.globalLock.Lock() + defer cache.globalLock.Unlock() + valSet1 := set.NewMapSet[any](mapSetInitSize) + valSet1.Add("value1") + valSet1.Add("value2") + node1 := newSetRBTreeCacheNode("key1", mapSetInitSize) + node1.value = valSet1 + cache.addNode(node1) + cache.deleteNode(node1) + return cache + }, + wantRet: 2, + }, + { + name: "wrong type", + startCache: func() *RBTreePriorityCache { + cache, _ := NewRBTreePriorityCache() + cache.globalLock.Lock() + defer cache.globalLock.Unlock() + cache.addNode(newKVRBTreeCacheNode("key1", "value1", 0)) + return cache + }, + key: "key1", + values: []any{"value1"}, + wantCache: func() *RBTreePriorityCache { + cache, _ := NewRBTreePriorityCache() + cache.globalLock.Lock() + defer cache.globalLock.Unlock() + cache.addNode(newKVRBTreeCacheNode("key1", "value1", 0)) + return cache + }, + wantRet: 0, + wantErr: errOnlySetCanSRem, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + startCache := tc.startCache() + value := startCache.SRem(context.Background(), tc.key, tc.values...) + assert.Equal(t, tc.wantErr, value.Err) + if value.Err != nil { + return + } + assert.Equal(t, tc.wantRet, value.Val) + assert.Equal(t, true, compareTwoRBTreeClient(startCache, tc.wantCache())) + }) + } +} + +func TestRBTreePriorityCache_IncrBy(t *testing.T) { + testCases := []struct { + name string + startCache func() *RBTreePriorityCache + key string + value int64 + wantCache func() *RBTreePriorityCache + wantRet int64 + wantErr error + }{ + { + name: "cache 0,miss,add 1", + startCache: func() *RBTreePriorityCache { + client, _ := NewRBTreePriorityCache() + return client + }, + key: "key1", + value: 1, + wantCache: func() *RBTreePriorityCache { + cache, _ := NewRBTreePriorityCache() + cache.globalLock.Lock() + defer cache.globalLock.Unlock() + node1 := newIntRBTreeCacheNode("key1") + node1.value = int64(1) + cache.addNode(node1) + return cache + }, + wantRet: 1, + }, + { + name: "cache 1,hit,value add 1", + startCache: func() *RBTreePriorityCache { + cache, _ := NewRBTreePriorityCache() + cache.globalLock.Lock() + defer cache.globalLock.Unlock() + node1 := newIntRBTreeCacheNode("key1") + node1.value = int64(1) + cache.addNode(node1) + return cache + }, + key: "key1", + value: 1, + wantCache: func() *RBTreePriorityCache { + cache, _ := NewRBTreePriorityCache() + cache.globalLock.Lock() + defer cache.globalLock.Unlock() + node1 := newIntRBTreeCacheNode("key1") + node1.value = int64(2) + cache.addNode(node1) + return cache + }, + wantRet: 2, + }, + { + name: "limit 1,cache 1,evict", + startCache: func() *RBTreePriorityCache { + cache, _ := NewRBTreePriorityCache(WithCacheLimit(1)) + cache.globalLock.Lock() + defer cache.globalLock.Unlock() + node1 := newIntRBTreeCacheNode("key1") + node1.value = int64(1) + cache.addNode(node1) + return cache + }, + key: "key2", + value: 1, + wantCache: func() *RBTreePriorityCache { + cache, _ := NewRBTreePriorityCache(WithCacheLimit(1)) + cache.globalLock.Lock() + defer cache.globalLock.Unlock() + node1 := newIntRBTreeCacheNode("key2") + node1.value = int64(1) + cache.addNode(node1) + return cache + }, + wantRet: 1, + }, + { + name: "wrong type", + startCache: func() *RBTreePriorityCache { + cache, _ := NewRBTreePriorityCache() + cache.globalLock.Lock() + defer cache.globalLock.Unlock() + cache.addNode(newKVRBTreeCacheNode("key1", "value1", 0)) + return cache + }, + key: "key1", + value: 1, + wantCache: func() *RBTreePriorityCache { + cache, _ := NewRBTreePriorityCache() + cache.globalLock.Lock() + defer cache.globalLock.Unlock() + cache.addNode(newKVRBTreeCacheNode("key1", "value1", 0)) + return cache + }, + wantErr: errOnlyNumCanIncrBy, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + startCache := tc.startCache() + value, err := startCache.IncrBy(context.Background(), tc.key, tc.value) + assert.Equal(t, tc.wantErr, err) + if err != nil { + return + } + assert.Equal(t, tc.wantRet, value) + assert.Equal(t, true, compareTwoRBTreeClient(startCache, tc.wantCache())) + }) + } +} + +func TestRBTreePriorityCache_DecrBy(t *testing.T) { + testCases := []struct { + name string + startCache func() *RBTreePriorityCache + key string + value int64 + wantCache func() *RBTreePriorityCache + wantRet int64 + wantErr error + }{ + { + name: "cache 0,miss,add 1", + startCache: func() *RBTreePriorityCache { + cache, _ := NewRBTreePriorityCache() + return cache + }, + key: "key1", + value: 1, + wantCache: func() *RBTreePriorityCache { + cache, _ := NewRBTreePriorityCache() + cache.globalLock.Lock() + defer cache.globalLock.Unlock() + node1 := newIntRBTreeCacheNode("key1") + node1.value = int64(-1) + cache.addNode(node1) + return cache + }, + wantRet: -1, + }, + { + name: "cache 1,hit,value decr 1", + startCache: func() *RBTreePriorityCache { + cache, _ := NewRBTreePriorityCache() + cache.globalLock.Lock() + defer cache.globalLock.Unlock() + node1 := newIntRBTreeCacheNode("key1") + node1.value = int64(1) + cache.addNode(node1) + return cache + }, + key: "key1", + value: 1, + wantCache: func() *RBTreePriorityCache { + cache, _ := NewRBTreePriorityCache() + cache.globalLock.Lock() + defer cache.globalLock.Unlock() + node1 := newIntRBTreeCacheNode("key1") + node1.value = int64(0) + cache.addNode(node1) + return cache + }, + wantRet: 0, + }, + { + name: "limit 1,cache 1,evict", + startCache: func() *RBTreePriorityCache { + cache, _ := NewRBTreePriorityCache(WithCacheLimit(1)) + cache.globalLock.Lock() + defer cache.globalLock.Unlock() + node1 := newIntRBTreeCacheNode("key1") + node1.value = int64(1) + cache.addNode(node1) + return cache + }, + key: "key2", + value: 1, + wantCache: func() *RBTreePriorityCache { + cache, _ := NewRBTreePriorityCache(WithCacheLimit(1)) + cache.globalLock.Lock() + defer cache.globalLock.Unlock() + node1 := newIntRBTreeCacheNode("key2") + node1.value = int64(-1) + cache.addNode(node1) + return cache + }, + wantRet: -1, + }, + { + name: "wrong type", + startCache: func() *RBTreePriorityCache { + cache, _ := NewRBTreePriorityCache() + cache.globalLock.Lock() + defer cache.globalLock.Unlock() + cache.addNode(newKVRBTreeCacheNode("key1", "value1", 0)) + return cache + }, + key: "key1", + value: 1, + wantCache: func() *RBTreePriorityCache { + cache, _ := NewRBTreePriorityCache() + cache.globalLock.Lock() + defer cache.globalLock.Unlock() + cache.addNode(newKVRBTreeCacheNode("key1", "value1", 0)) + return cache + }, + wantErr: errOnlyNumCanDecrBy, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + startCache := tc.startCache() + value, err := startCache.DecrBy(context.Background(), tc.key, tc.value) + assert.Equal(t, tc.wantErr, err) + if err != nil { + return + } + assert.Equal(t, tc.wantRet, value) + assert.Equal(t, true, compareTwoRBTreeClient(startCache, tc.wantCache())) + }) + } +} + +func TestRBTreePriorityCache_autoClean(t *testing.T) { + cache, _ := NewRBTreePriorityCache() + key := "key" + value := "value" + + wg := sync.WaitGroup{} + for i := 1; i <= 6; i++ { + wg.Add(1) + j := i + go func() { + tempKey := fmt.Sprintf("%s%d", key, j) + tempValue := fmt.Sprintf("%s%d", value, j) + _ = cache.Set(context.Background(), tempKey, tempValue, time.Duration(j)*time.Second) + wg.Done() + }() + } + wg.Wait() + + value1 := cache.Get(context.Background(), "key1") + value1Str, _ := value1.String() + assert.Equal(t, "value1", value1Str) + + value6 := cache.Get(context.Background(), "key6") + value6Str, _ := value6.String() + assert.Equal(t, "value6", value6Str) + + time.Sleep(3 * time.Second) + + value1 = cache.Get(context.Background(), "key1") + assert.Equal(t, errs.ErrKeyNotExist, value1.Err) + + value6 = cache.Get(context.Background(), "key6") + value6Str, _ = value6.String() + assert.Equal(t, "value6", value6Str) +}