├── .prettierrc.yml ├── CHANGELOG.md ├── LICENSE ├── Makefile ├── README.md ├── bench_test.go ├── cache.go ├── cache_test.go ├── example_cache_test.go ├── go.mod ├── go.sum ├── local.go └── local_test.go /.prettierrc.yml: -------------------------------------------------------------------------------- 1 | semi: false 2 | singleQuote: true 3 | proseWrap: always 4 | printWidth: 100 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | > :heart: [**Uptrace.dev** - distributed traces, logs, and errors in one place](https://uptrace.dev) 4 | 5 | ## v8 6 | 7 | - Added s2 (snappy) compression. That means that v8 can't read the data set by v7. 8 | - Replaced LRU with TinyLFU for local cache. 9 | - Requires go-redis v8. 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 The github.com/go-redis/cache Contributors. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are 6 | met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | * Redistributions in binary form must reproduce the above 11 | copyright notice, this list of conditions and the following disclaimer 12 | in the documentation and/or other materials provided with the 13 | distribution. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 16 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 17 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 18 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 19 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 20 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 21 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 22 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 23 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: 2 | go test ./... 3 | go test ./... -short -race 4 | go test ./... -run=NONE -bench=. -benchmem 5 | env GOOS=linux GOARCH=386 go test ./... 6 | golangci-lint run 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Redis cache library for Golang 2 | 3 | [![Build Status](https://travis-ci.org/go-redis/cache.svg)](https://travis-ci.org/go-redis/cache) 4 | [![GoDoc](https://godoc.org/github.com/go-redis/cache?status.svg)](https://pkg.go.dev/github.com/go-redis/cache/v9?tab=doc) 5 | 6 | > go-redis/cache is brought to you by :star: 7 | > [**uptrace/uptrace**](https://github.com/uptrace/uptrace). Uptrace is an open source and blazingly 8 | > fast [distributed tracing tool](https://get.uptrace.dev/) powered by OpenTelemetry and ClickHouse. 9 | > Give it a star as well! 10 | 11 | go-redis/cache library implements a cache using Redis as a key/value storage. It uses 12 | [MessagePack](https://github.com/vmihailenco/msgpack) to marshal values. 13 | 14 | Optionally, you can use [TinyLFU](https://github.com/vmihailenco/go-tinylfu) or any other 15 | [cache algorithm](https://github.com/vmihailenco/go-cache-benchmark) as a local in-process cache. 16 | 17 | If you are interested in monitoring cache hit rate, see the guide for 18 | [Monitoring using OpenTelemetry Metrics](https://blog.uptrace.dev/posts/opentelemetry-metrics-cache-stats/). 19 | 20 | ## Installation 21 | 22 | go-redis/cache supports 2 last Go versions and requires a Go version with 23 | [modules](https://github.com/golang/go/wiki/Modules) support. So make sure to initialize a Go 24 | module: 25 | 26 | ```shell 27 | go mod init github.com/my/repo 28 | ``` 29 | 30 | And then install go-redis/cache/v9 (note _v9_ in the import; omitting it is a popular mistake): 31 | 32 | ```shell 33 | go get github.com/go-redis/cache/v9 34 | ``` 35 | 36 | ## Quickstart 37 | 38 | ```go 39 | package cache_test 40 | 41 | import ( 42 | "context" 43 | "fmt" 44 | "time" 45 | 46 | "github.com/redis/go-redis/v9" 47 | "github.com/go-redis/cache/v9" 48 | ) 49 | 50 | type Object struct { 51 | Str string 52 | Num int 53 | } 54 | 55 | func Example_basicUsage() { 56 | ring := redis.NewRing(&redis.RingOptions{ 57 | Addrs: map[string]string{ 58 | "server1": ":6379", 59 | "server2": ":6380", 60 | }, 61 | }) 62 | 63 | mycache := cache.New(&cache.Options{ 64 | Redis: ring, 65 | LocalCache: cache.NewTinyLFU(1000, time.Minute), 66 | }) 67 | 68 | ctx := context.TODO() 69 | key := "mykey" 70 | obj := &Object{ 71 | Str: "mystring", 72 | Num: 42, 73 | } 74 | 75 | if err := mycache.Set(&cache.Item{ 76 | Ctx: ctx, 77 | Key: key, 78 | Value: obj, 79 | TTL: time.Hour, 80 | }); err != nil { 81 | panic(err) 82 | } 83 | 84 | var wanted Object 85 | if err := mycache.Get(ctx, key, &wanted); err == nil { 86 | fmt.Println(wanted) 87 | } 88 | 89 | // Output: {mystring 42} 90 | } 91 | 92 | func Example_advancedUsage() { 93 | ring := redis.NewRing(&redis.RingOptions{ 94 | Addrs: map[string]string{ 95 | "server1": ":6379", 96 | "server2": ":6380", 97 | }, 98 | }) 99 | 100 | mycache := cache.New(&cache.Options{ 101 | Redis: ring, 102 | LocalCache: cache.NewTinyLFU(1000, time.Minute), 103 | }) 104 | 105 | obj := new(Object) 106 | err := mycache.Once(&cache.Item{ 107 | Key: "mykey", 108 | Value: obj, // destination 109 | Do: func(*cache.Item) (interface{}, error) { 110 | return &Object{ 111 | Str: "mystring", 112 | Num: 42, 113 | }, nil 114 | }, 115 | }) 116 | if err != nil { 117 | panic(err) 118 | } 119 | fmt.Println(obj) 120 | // Output: &{mystring 42} 121 | } 122 | ``` 123 | -------------------------------------------------------------------------------- /bench_test.go: -------------------------------------------------------------------------------- 1 | package cache_test 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | "github.com/go-redis/cache/v9" 8 | ) 9 | 10 | func BenchmarkOnce(b *testing.B) { 11 | mycache := newCacheWithLocal(newRing()) 12 | obj := &Object{ 13 | Str: strings.Repeat("my very large string", 10), 14 | Num: 42, 15 | } 16 | 17 | b.ResetTimer() 18 | 19 | b.RunParallel(func(pb *testing.PB) { 20 | for pb.Next() { 21 | var dst Object 22 | err := mycache.Once(&cache.Item{ 23 | Key: "bench-once", 24 | Value: &dst, 25 | Do: func(*cache.Item) (interface{}, error) { 26 | return obj, nil 27 | }, 28 | }) 29 | if err != nil { 30 | b.Fatal(err) 31 | } 32 | if dst.Num != 42 { 33 | b.Fatalf("%d != 42", dst.Num) 34 | } 35 | } 36 | }) 37 | } 38 | 39 | func BenchmarkSet(b *testing.B) { 40 | mycache := newCacheWithLocal(newRing()) 41 | obj := &Object{ 42 | Str: strings.Repeat("my very large string", 10), 43 | Num: 42, 44 | } 45 | 46 | b.ResetTimer() 47 | 48 | b.RunParallel(func(pb *testing.PB) { 49 | for pb.Next() { 50 | if err := mycache.Set(&cache.Item{ 51 | Key: "bench-set", 52 | Value: obj, 53 | }); err != nil { 54 | b.Fatal(err) 55 | } 56 | } 57 | }) 58 | } 59 | -------------------------------------------------------------------------------- /cache.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "log" 8 | "sync/atomic" 9 | "time" 10 | 11 | "github.com/klauspost/compress/s2" 12 | "github.com/redis/go-redis/v9" 13 | "github.com/vmihailenco/msgpack/v5" 14 | "golang.org/x/sync/singleflight" 15 | ) 16 | 17 | const ( 18 | compressionThreshold = 64 19 | timeLen = 4 20 | ) 21 | 22 | const ( 23 | noCompression = 0x0 24 | s2Compression = 0x1 25 | ) 26 | 27 | var ( 28 | ErrCacheMiss = errors.New("cache: key is missing") 29 | errRedisLocalCacheNil = errors.New("cache: both Redis and LocalCache are nil") 30 | ) 31 | 32 | type rediser interface { 33 | Set(ctx context.Context, key string, value interface{}, ttl time.Duration) *redis.StatusCmd 34 | SetXX(ctx context.Context, key string, value interface{}, ttl time.Duration) *redis.BoolCmd 35 | SetNX(ctx context.Context, key string, value interface{}, ttl time.Duration) *redis.BoolCmd 36 | 37 | Get(ctx context.Context, key string) *redis.StringCmd 38 | Del(ctx context.Context, keys ...string) *redis.IntCmd 39 | } 40 | 41 | type Item struct { 42 | Ctx context.Context 43 | 44 | Key string 45 | Value interface{} 46 | 47 | // TTL is the cache expiration time. 48 | // Default TTL is 1 hour. 49 | TTL time.Duration 50 | 51 | // Do returns value to be cached. 52 | Do func(*Item) (interface{}, error) 53 | 54 | // SetXX only sets the key if it already exists. 55 | SetXX bool 56 | 57 | // SetNX only sets the key if it does not already exist. 58 | SetNX bool 59 | 60 | // SkipLocalCache skips local cache as if it is not set. 61 | SkipLocalCache bool 62 | } 63 | 64 | func (item *Item) Context() context.Context { 65 | if item.Ctx == nil { 66 | return context.Background() 67 | } 68 | return item.Ctx 69 | } 70 | 71 | func (item *Item) value() (interface{}, error) { 72 | if item.Do != nil { 73 | return item.Do(item) 74 | } 75 | if item.Value != nil { 76 | return item.Value, nil 77 | } 78 | return nil, nil 79 | } 80 | 81 | func (item *Item) ttl() time.Duration { 82 | const defaultTTL = time.Hour 83 | 84 | if item.TTL < 0 { 85 | return 0 86 | } 87 | 88 | if item.TTL != 0 { 89 | if item.TTL < time.Second { 90 | log.Printf("too short TTL for key=%q: %s", item.Key, item.TTL) 91 | return defaultTTL 92 | } 93 | return item.TTL 94 | } 95 | 96 | return defaultTTL 97 | } 98 | 99 | //------------------------------------------------------------------------------ 100 | type ( 101 | MarshalFunc func(interface{}) ([]byte, error) 102 | UnmarshalFunc func([]byte, interface{}) error 103 | ) 104 | 105 | type Options struct { 106 | Redis rediser 107 | LocalCache LocalCache 108 | StatsEnabled bool 109 | Marshal MarshalFunc 110 | Unmarshal UnmarshalFunc 111 | } 112 | 113 | type Cache struct { 114 | opt *Options 115 | 116 | group singleflight.Group 117 | 118 | marshal MarshalFunc 119 | unmarshal UnmarshalFunc 120 | 121 | hits uint64 122 | misses uint64 123 | } 124 | 125 | func New(opt *Options) *Cache { 126 | cacher := &Cache{ 127 | opt: opt, 128 | } 129 | 130 | if opt.Marshal == nil { 131 | cacher.marshal = cacher._marshal 132 | } else { 133 | cacher.marshal = opt.Marshal 134 | } 135 | 136 | if opt.Unmarshal == nil { 137 | cacher.unmarshal = cacher._unmarshal 138 | } else { 139 | cacher.unmarshal = opt.Unmarshal 140 | } 141 | return cacher 142 | } 143 | 144 | // Set caches the item. 145 | func (cd *Cache) Set(item *Item) error { 146 | _, _, err := cd.set(item) 147 | return err 148 | } 149 | 150 | func (cd *Cache) set(item *Item) ([]byte, bool, error) { 151 | value, err := item.value() 152 | if err != nil { 153 | return nil, false, err 154 | } 155 | 156 | b, err := cd.Marshal(value) 157 | if err != nil { 158 | return nil, false, err 159 | } 160 | 161 | if cd.opt.LocalCache != nil && !item.SkipLocalCache { 162 | cd.opt.LocalCache.Set(item.Key, b) 163 | } 164 | 165 | if cd.opt.Redis == nil { 166 | if cd.opt.LocalCache == nil { 167 | return b, true, errRedisLocalCacheNil 168 | } 169 | return b, true, nil 170 | } 171 | 172 | ttl := item.ttl() 173 | if ttl == 0 { 174 | return b, true, nil 175 | } 176 | 177 | if item.SetXX { 178 | return b, true, cd.opt.Redis.SetXX(item.Context(), item.Key, b, ttl).Err() 179 | } 180 | if item.SetNX { 181 | return b, true, cd.opt.Redis.SetNX(item.Context(), item.Key, b, ttl).Err() 182 | } 183 | return b, true, cd.opt.Redis.Set(item.Context(), item.Key, b, ttl).Err() 184 | } 185 | 186 | // Exists reports whether value for the given key exists. 187 | func (cd *Cache) Exists(ctx context.Context, key string) bool { 188 | _, err := cd.getBytes(ctx, key, false) 189 | return err == nil 190 | } 191 | 192 | // Get gets the value for the given key. 193 | func (cd *Cache) Get(ctx context.Context, key string, value interface{}) error { 194 | return cd.get(ctx, key, value, false) 195 | } 196 | 197 | // Get gets the value for the given key skipping local cache. 198 | func (cd *Cache) GetSkippingLocalCache( 199 | ctx context.Context, key string, value interface{}, 200 | ) error { 201 | return cd.get(ctx, key, value, true) 202 | } 203 | 204 | func (cd *Cache) get( 205 | ctx context.Context, 206 | key string, 207 | value interface{}, 208 | skipLocalCache bool, 209 | ) error { 210 | b, err := cd.getBytes(ctx, key, skipLocalCache) 211 | if err != nil { 212 | return err 213 | } 214 | return cd.unmarshal(b, value) 215 | } 216 | 217 | func (cd *Cache) getBytes(ctx context.Context, key string, skipLocalCache bool) ([]byte, error) { 218 | if !skipLocalCache && cd.opt.LocalCache != nil { 219 | b, ok := cd.opt.LocalCache.Get(key) 220 | if ok { 221 | return b, nil 222 | } 223 | } 224 | 225 | if cd.opt.Redis == nil { 226 | if cd.opt.LocalCache == nil { 227 | return nil, errRedisLocalCacheNil 228 | } 229 | return nil, ErrCacheMiss 230 | } 231 | 232 | b, err := cd.opt.Redis.Get(ctx, key).Bytes() 233 | if err != nil { 234 | if cd.opt.StatsEnabled { 235 | atomic.AddUint64(&cd.misses, 1) 236 | } 237 | if err == redis.Nil { 238 | return nil, ErrCacheMiss 239 | } 240 | return nil, err 241 | } 242 | 243 | if cd.opt.StatsEnabled { 244 | atomic.AddUint64(&cd.hits, 1) 245 | } 246 | 247 | if !skipLocalCache && cd.opt.LocalCache != nil { 248 | cd.opt.LocalCache.Set(key, b) 249 | } 250 | return b, nil 251 | } 252 | 253 | // Once gets the item.Value for the given item.Key from the cache or 254 | // executes, caches, and returns the results of the given item.Func, 255 | // making sure that only one execution is in-flight for a given item.Key 256 | // at a time. If a duplicate comes in, the duplicate caller waits for the 257 | // original to complete and receives the same results. 258 | func (cd *Cache) Once(item *Item) error { 259 | b, cached, err := cd.getSetItemBytesOnce(item) 260 | if err != nil { 261 | return err 262 | } 263 | 264 | if item.Value == nil || len(b) == 0 { 265 | return nil 266 | } 267 | 268 | if err := cd.unmarshal(b, item.Value); err != nil { 269 | if cached { 270 | _ = cd.Delete(item.Context(), item.Key) 271 | return cd.Once(item) 272 | } 273 | return err 274 | } 275 | 276 | return nil 277 | } 278 | 279 | func (cd *Cache) getSetItemBytesOnce(item *Item) (b []byte, cached bool, err error) { 280 | if cd.opt.LocalCache != nil { 281 | b, ok := cd.opt.LocalCache.Get(item.Key) 282 | if ok { 283 | return b, true, nil 284 | } 285 | } 286 | 287 | v, err, _ := cd.group.Do(item.Key, func() (interface{}, error) { 288 | b, err := cd.getBytes(item.Context(), item.Key, item.SkipLocalCache) 289 | if err == nil { 290 | cached = true 291 | return b, nil 292 | } 293 | 294 | b, ok, err := cd.set(item) 295 | if ok { 296 | return b, nil 297 | } 298 | return nil, err 299 | }) 300 | if err != nil { 301 | return nil, false, err 302 | } 303 | return v.([]byte), cached, nil 304 | } 305 | 306 | func (cd *Cache) Delete(ctx context.Context, key string) error { 307 | if cd.opt.LocalCache != nil { 308 | cd.opt.LocalCache.Del(key) 309 | } 310 | 311 | if cd.opt.Redis == nil { 312 | if cd.opt.LocalCache == nil { 313 | return errRedisLocalCacheNil 314 | } 315 | return nil 316 | } 317 | 318 | _, err := cd.opt.Redis.Del(ctx, key).Result() 319 | return err 320 | } 321 | 322 | func (cd *Cache) DeleteFromLocalCache(key string) { 323 | if cd.opt.LocalCache != nil { 324 | cd.opt.LocalCache.Del(key) 325 | } 326 | } 327 | 328 | func (cd *Cache) Marshal(value interface{}) ([]byte, error) { 329 | return cd.marshal(value) 330 | } 331 | 332 | func (cd *Cache) _marshal(value interface{}) ([]byte, error) { 333 | switch value := value.(type) { 334 | case nil: 335 | return nil, nil 336 | case []byte: 337 | return value, nil 338 | case string: 339 | return []byte(value), nil 340 | } 341 | 342 | b, err := msgpack.Marshal(value) 343 | if err != nil { 344 | return nil, err 345 | } 346 | 347 | return compress(b), nil 348 | } 349 | 350 | func compress(data []byte) []byte { 351 | if len(data) < compressionThreshold { 352 | n := len(data) + 1 353 | b := make([]byte, n, n+timeLen) 354 | copy(b, data) 355 | b[len(b)-1] = noCompression 356 | return b 357 | } 358 | 359 | n := s2.MaxEncodedLen(len(data)) + 1 360 | b := make([]byte, n, n+timeLen) 361 | b = s2.Encode(b, data) 362 | b = append(b, s2Compression) 363 | return b 364 | } 365 | 366 | func (cd *Cache) Unmarshal(b []byte, value interface{}) error { 367 | return cd.unmarshal(b, value) 368 | } 369 | 370 | func (cd *Cache) _unmarshal(b []byte, value interface{}) error { 371 | if len(b) == 0 { 372 | return nil 373 | } 374 | 375 | switch value := value.(type) { 376 | case nil: 377 | return nil 378 | case *[]byte: 379 | clone := make([]byte, len(b)) 380 | copy(clone, b) 381 | *value = clone 382 | return nil 383 | case *string: 384 | *value = string(b) 385 | return nil 386 | } 387 | 388 | switch c := b[len(b)-1]; c { 389 | case noCompression: 390 | b = b[:len(b)-1] 391 | case s2Compression: 392 | b = b[:len(b)-1] 393 | 394 | var err error 395 | b, err = s2.Decode(nil, b) 396 | if err != nil { 397 | return err 398 | } 399 | default: 400 | return fmt.Errorf("unknown compression method: %x", c) 401 | } 402 | 403 | return msgpack.Unmarshal(b, value) 404 | } 405 | 406 | //------------------------------------------------------------------------------ 407 | 408 | type Stats struct { 409 | Hits uint64 410 | Misses uint64 411 | } 412 | 413 | // Stats returns cache statistics. 414 | func (cd *Cache) Stats() *Stats { 415 | if !cd.opt.StatsEnabled { 416 | return nil 417 | } 418 | return &Stats{ 419 | Hits: atomic.LoadUint64(&cd.hits), 420 | Misses: atomic.LoadUint64(&cd.misses), 421 | } 422 | } 423 | -------------------------------------------------------------------------------- /cache_test.go: -------------------------------------------------------------------------------- 1 | package cache_test 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "io" 7 | "sync" 8 | "sync/atomic" 9 | "testing" 10 | "time" 11 | 12 | . "github.com/onsi/ginkgo" 13 | . "github.com/onsi/gomega" 14 | "github.com/redis/go-redis/v9" 15 | 16 | "github.com/go-redis/cache/v9" 17 | ) 18 | 19 | func TestGinkgo(t *testing.T) { 20 | RegisterFailHandler(Fail) 21 | RunSpecs(t, "cache") 22 | } 23 | 24 | func perform(n int, cbs ...func(int)) { 25 | var wg sync.WaitGroup 26 | for _, cb := range cbs { 27 | for i := 0; i < n; i++ { 28 | wg.Add(1) 29 | go func(cb func(int), i int) { 30 | defer wg.Done() 31 | defer GinkgoRecover() 32 | 33 | cb(i) 34 | }(cb, i) 35 | } 36 | } 37 | wg.Wait() 38 | } 39 | 40 | var _ = Describe("Cache", func() { 41 | ctx := context.TODO() 42 | 43 | const key = "mykey" 44 | var obj *Object 45 | 46 | var rdb *redis.Ring 47 | var mycache *cache.Cache 48 | 49 | testCache := func() { 50 | It("Gets and Sets nil", func() { 51 | err := mycache.Set(&cache.Item{ 52 | Key: key, 53 | TTL: time.Hour, 54 | }) 55 | Expect(err).NotTo(HaveOccurred()) 56 | 57 | err = mycache.Get(ctx, key, nil) 58 | Expect(err).NotTo(HaveOccurred()) 59 | 60 | Expect(mycache.Exists(ctx, key)).To(BeTrue()) 61 | }) 62 | 63 | It("Deletes key", func() { 64 | err := mycache.Set(&cache.Item{ 65 | Ctx: ctx, 66 | Key: key, 67 | TTL: time.Hour, 68 | }) 69 | Expect(err).NotTo(HaveOccurred()) 70 | 71 | Expect(mycache.Exists(ctx, key)).To(BeTrue()) 72 | 73 | err = mycache.Delete(ctx, key) 74 | Expect(err).NotTo(HaveOccurred()) 75 | 76 | err = mycache.Get(ctx, key, nil) 77 | Expect(err).To(Equal(cache.ErrCacheMiss)) 78 | 79 | Expect(mycache.Exists(ctx, key)).To(BeFalse()) 80 | }) 81 | 82 | It("Gets and Sets data", func() { 83 | err := mycache.Set(&cache.Item{ 84 | Ctx: ctx, 85 | Key: key, 86 | Value: obj, 87 | TTL: time.Hour, 88 | }) 89 | Expect(err).NotTo(HaveOccurred()) 90 | 91 | wanted := new(Object) 92 | err = mycache.Get(ctx, key, wanted) 93 | Expect(err).NotTo(HaveOccurred()) 94 | Expect(wanted).To(Equal(obj)) 95 | 96 | Expect(mycache.Exists(ctx, key)).To(BeTrue()) 97 | }) 98 | 99 | It("Sets string as is", func() { 100 | value := "str_value" 101 | 102 | err := mycache.Set(&cache.Item{ 103 | Ctx: ctx, 104 | Key: key, 105 | Value: value, 106 | }) 107 | Expect(err).NotTo(HaveOccurred()) 108 | 109 | var dst string 110 | err = mycache.Get(ctx, key, &dst) 111 | Expect(err).NotTo(HaveOccurred()) 112 | Expect(dst).To(Equal(value)) 113 | }) 114 | 115 | It("Sets bytes as is", func() { 116 | value := []byte("str_value") 117 | 118 | err := mycache.Set(&cache.Item{ 119 | Ctx: ctx, 120 | Key: key, 121 | Value: value, 122 | }) 123 | Expect(err).NotTo(HaveOccurred()) 124 | 125 | var dst []byte 126 | err = mycache.Get(ctx, key, &dst) 127 | Expect(err).NotTo(HaveOccurred()) 128 | Expect(dst).To(Equal(value)) 129 | }) 130 | 131 | It("can be used with Incr", func() { 132 | if rdb == nil { 133 | return 134 | } 135 | 136 | value := "123" 137 | 138 | err := mycache.Set(&cache.Item{ 139 | Ctx: ctx, 140 | Key: key, 141 | Value: value, 142 | }) 143 | Expect(err).NotTo(HaveOccurred()) 144 | 145 | n, err := rdb.Incr(ctx, key).Result() 146 | Expect(err).NotTo(HaveOccurred()) 147 | Expect(n).To(Equal(int64(124))) 148 | }) 149 | 150 | Describe("Once func", func() { 151 | It("calls Func when cache fails", func() { 152 | err := mycache.Set(&cache.Item{ 153 | Ctx: ctx, 154 | Key: key, 155 | Value: int64(0), 156 | }) 157 | Expect(err).NotTo(HaveOccurred()) 158 | 159 | var got bool 160 | err = mycache.Get(ctx, key, &got) 161 | Expect(err).To(MatchError("msgpack: invalid code=d3 decoding bool")) 162 | 163 | err = mycache.Once(&cache.Item{ 164 | Ctx: ctx, 165 | Key: key, 166 | Value: &got, 167 | Do: func(*cache.Item) (interface{}, error) { 168 | return true, nil 169 | }, 170 | }) 171 | Expect(err).NotTo(HaveOccurred()) 172 | Expect(got).To(BeTrue()) 173 | 174 | got = false 175 | err = mycache.Get(ctx, key, &got) 176 | Expect(err).NotTo(HaveOccurred()) 177 | Expect(got).To(BeTrue()) 178 | }) 179 | 180 | It("does not cache when Func fails", func() { 181 | perform(100, func(int) { 182 | var got bool 183 | err := mycache.Once(&cache.Item{ 184 | Ctx: ctx, 185 | Key: key, 186 | Value: &got, 187 | Do: func(*cache.Item) (interface{}, error) { 188 | return nil, io.EOF 189 | }, 190 | }) 191 | Expect(err).To(Equal(io.EOF)) 192 | Expect(got).To(BeFalse()) 193 | }) 194 | 195 | var got bool 196 | err := mycache.Get(ctx, key, &got) 197 | Expect(err).To(Equal(cache.ErrCacheMiss)) 198 | 199 | err = mycache.Once(&cache.Item{ 200 | Ctx: ctx, 201 | Key: key, 202 | Value: &got, 203 | Do: func(*cache.Item) (interface{}, error) { 204 | return true, nil 205 | }, 206 | }) 207 | Expect(err).NotTo(HaveOccurred()) 208 | Expect(got).To(BeTrue()) 209 | }) 210 | 211 | It("works with Value", func() { 212 | var callCount int64 213 | perform(100, func(int) { 214 | got := new(Object) 215 | err := mycache.Once(&cache.Item{ 216 | Ctx: ctx, 217 | Key: key, 218 | Value: got, 219 | Do: func(*cache.Item) (interface{}, error) { 220 | atomic.AddInt64(&callCount, 1) 221 | return obj, nil 222 | }, 223 | }) 224 | Expect(err).NotTo(HaveOccurred()) 225 | Expect(got).To(Equal(obj)) 226 | }) 227 | Expect(callCount).To(Equal(int64(1))) 228 | }) 229 | 230 | It("works with ptr and non-ptr", func() { 231 | var callCount int64 232 | perform(100, func(int) { 233 | got := new(Object) 234 | err := mycache.Once(&cache.Item{ 235 | Ctx: ctx, 236 | Key: key, 237 | Value: got, 238 | Do: func(*cache.Item) (interface{}, error) { 239 | atomic.AddInt64(&callCount, 1) 240 | return *obj, nil 241 | }, 242 | }) 243 | Expect(err).NotTo(HaveOccurred()) 244 | Expect(got).To(Equal(obj)) 245 | }) 246 | Expect(callCount).To(Equal(int64(1))) 247 | }) 248 | 249 | It("works with bool", func() { 250 | var callCount int64 251 | perform(100, func(int) { 252 | var got bool 253 | err := mycache.Once(&cache.Item{ 254 | Ctx: ctx, 255 | Key: key, 256 | Value: &got, 257 | Do: func(*cache.Item) (interface{}, error) { 258 | atomic.AddInt64(&callCount, 1) 259 | return true, nil 260 | }, 261 | }) 262 | Expect(err).NotTo(HaveOccurred()) 263 | Expect(got).To(BeTrue()) 264 | }) 265 | Expect(callCount).To(Equal(int64(1))) 266 | }) 267 | 268 | It("works without Value and nil result", func() { 269 | var callCount int64 270 | perform(100, func(int) { 271 | err := mycache.Once(&cache.Item{ 272 | Ctx: ctx, 273 | Key: key, 274 | Do: func(*cache.Item) (interface{}, error) { 275 | atomic.AddInt64(&callCount, 1) 276 | return nil, nil 277 | }, 278 | }) 279 | Expect(err).NotTo(HaveOccurred()) 280 | }) 281 | Expect(callCount).To(Equal(int64(1))) 282 | }) 283 | 284 | It("works without Value and error result", func() { 285 | var callCount int64 286 | perform(100, func(int) { 287 | err := mycache.Once(&cache.Item{ 288 | Ctx: ctx, 289 | Key: key, 290 | Do: func(*cache.Item) (interface{}, error) { 291 | time.Sleep(100 * time.Millisecond) 292 | atomic.AddInt64(&callCount, 1) 293 | return nil, errors.New("error stub") 294 | }, 295 | }) 296 | Expect(err).To(MatchError("error stub")) 297 | }) 298 | Expect(callCount).To(Equal(int64(1))) 299 | }) 300 | 301 | It("does not cache error result", func() { 302 | var callCount int64 303 | do := func(sleep time.Duration) (int, error) { 304 | var n int 305 | err := mycache.Once(&cache.Item{ 306 | Ctx: ctx, 307 | Key: key, 308 | Value: &n, 309 | Do: func(*cache.Item) (interface{}, error) { 310 | time.Sleep(sleep) 311 | 312 | n := atomic.AddInt64(&callCount, 1) 313 | if n == 1 { 314 | return nil, errors.New("error stub") 315 | } 316 | return 42, nil 317 | }, 318 | }) 319 | if err != nil { 320 | return 0, err 321 | } 322 | return n, nil 323 | } 324 | 325 | perform(100, func(int) { 326 | n, err := do(100 * time.Millisecond) 327 | Expect(err).To(MatchError("error stub")) 328 | Expect(n).To(Equal(0)) 329 | }) 330 | 331 | perform(100, func(int) { 332 | n, err := do(0) 333 | Expect(err).NotTo(HaveOccurred()) 334 | Expect(n).To(Equal(42)) 335 | }) 336 | 337 | Expect(callCount).To(Equal(int64(2))) 338 | }) 339 | 340 | It("skips Set when TTL = -1", func() { 341 | key := "skip-set" 342 | 343 | var value string 344 | err := mycache.Once(&cache.Item{ 345 | Ctx: ctx, 346 | Key: key, 347 | Value: &value, 348 | Do: func(item *cache.Item) (interface{}, error) { 349 | item.TTL = -1 350 | return "hello", nil 351 | }, 352 | }) 353 | Expect(err).NotTo(HaveOccurred()) 354 | Expect(value).To(Equal("hello")) 355 | 356 | if rdb != nil { 357 | exists, err := rdb.Exists(ctx, key).Result() 358 | Expect(err).NotTo(HaveOccurred()) 359 | Expect(exists).To(Equal(int64(0))) 360 | } 361 | }) 362 | }) 363 | } 364 | 365 | BeforeEach(func() { 366 | obj = &Object{ 367 | Str: "mystring", 368 | Num: 42, 369 | } 370 | }) 371 | 372 | Context("without LocalCache", func() { 373 | BeforeEach(func() { 374 | rdb = newRing() 375 | mycache = newCache(rdb) 376 | }) 377 | 378 | testCache() 379 | }) 380 | 381 | Context("with LocalCache", func() { 382 | BeforeEach(func() { 383 | rdb = newRing() 384 | mycache = newCacheWithLocal(rdb) 385 | }) 386 | 387 | testCache() 388 | }) 389 | 390 | Context("with LocalCache and without Redis", func() { 391 | BeforeEach(func() { 392 | rdb = nil 393 | mycache = cache.New(&cache.Options{ 394 | LocalCache: cache.NewTinyLFU(1000, time.Minute), 395 | }) 396 | }) 397 | 398 | testCache() 399 | }) 400 | }) 401 | 402 | func newRing() *redis.Ring { 403 | ctx := context.TODO() 404 | ring := redis.NewRing(&redis.RingOptions{ 405 | Addrs: map[string]string{ 406 | "server1": ":6379", 407 | }, 408 | }) 409 | _ = ring.ForEachShard(ctx, func(ctx context.Context, client *redis.Client) error { 410 | return client.FlushDB(ctx).Err() 411 | }) 412 | return ring 413 | } 414 | 415 | func newCache(rdb *redis.Ring) *cache.Cache { 416 | return cache.New(&cache.Options{ 417 | Redis: rdb, 418 | }) 419 | } 420 | 421 | func newCacheWithLocal(rdb *redis.Ring) *cache.Cache { 422 | return cache.New(&cache.Options{ 423 | Redis: rdb, 424 | LocalCache: cache.NewTinyLFU(1000, time.Minute), 425 | }) 426 | } 427 | -------------------------------------------------------------------------------- /example_cache_test.go: -------------------------------------------------------------------------------- 1 | package cache_test 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/redis/go-redis/v9" 9 | 10 | "github.com/go-redis/cache/v9" 11 | ) 12 | 13 | type Object struct { 14 | Str string 15 | Num int 16 | } 17 | 18 | func Example_basicUsage() { 19 | ring := redis.NewRing(&redis.RingOptions{ 20 | Addrs: map[string]string{ 21 | "server1": ":6379", 22 | "server2": ":6380", 23 | }, 24 | }) 25 | 26 | mycache := cache.New(&cache.Options{ 27 | Redis: ring, 28 | LocalCache: cache.NewTinyLFU(1000, time.Minute), 29 | }) 30 | 31 | ctx := context.TODO() 32 | key := "mykey" 33 | obj := &Object{ 34 | Str: "mystring", 35 | Num: 42, 36 | } 37 | 38 | if err := mycache.Set(&cache.Item{ 39 | Ctx: ctx, 40 | Key: key, 41 | Value: obj, 42 | TTL: time.Hour, 43 | }); err != nil { 44 | panic(err) 45 | } 46 | 47 | var wanted Object 48 | if err := mycache.Get(ctx, key, &wanted); err == nil { 49 | fmt.Println(wanted) 50 | } 51 | 52 | // Output: {mystring 42} 53 | } 54 | 55 | func Example_advancedUsage() { 56 | ring := redis.NewRing(&redis.RingOptions{ 57 | Addrs: map[string]string{ 58 | "server1": ":6379", 59 | "server2": ":6380", 60 | }, 61 | }) 62 | 63 | mycache := cache.New(&cache.Options{ 64 | Redis: ring, 65 | LocalCache: cache.NewTinyLFU(1000, time.Minute), 66 | }) 67 | 68 | obj := new(Object) 69 | err := mycache.Once(&cache.Item{ 70 | Key: "mykey", 71 | Value: obj, // destination 72 | Do: func(*cache.Item) (interface{}, error) { 73 | return &Object{ 74 | Str: "mystring", 75 | Num: 42, 76 | }, nil 77 | }, 78 | }) 79 | if err != nil { 80 | panic(err) 81 | } 82 | fmt.Println(obj) 83 | // Output: &{mystring 42} 84 | } 85 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/go-redis/cache/v9 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/klauspost/compress v1.13.6 7 | github.com/kr/pretty v0.3.0 // indirect 8 | github.com/onsi/ginkgo v1.16.5 9 | github.com/onsi/gomega v1.25.0 10 | github.com/redis/go-redis/v9 v9.0.5 11 | github.com/vmihailenco/go-tinylfu v0.2.2 12 | github.com/vmihailenco/msgpack/v5 v5.3.4 13 | golang.org/x/sync v0.1.0 14 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect 15 | ) 16 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/bsm/ginkgo/v2 v2.7.0 h1:ItPMPH90RbmZJt5GtkcNvIRuGEdwlBItdNVoyzaNQao= 2 | github.com/bsm/ginkgo/v2 v2.7.0/go.mod h1:AiKlXPm7ItEHNc/2+OkrNG4E0ITzojb9/xWzvQ9XZ9w= 3 | github.com/bsm/gomega v1.26.0 h1:LhQm+AFcgV2M0WyKroMASzAzCAJVpAxQXv4SaI9a69Y= 4 | github.com/bsm/gomega v1.26.0/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= 5 | github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 6 | github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= 7 | github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 8 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= 9 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= 10 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 11 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 12 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 13 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 14 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 15 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= 16 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= 17 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 18 | github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= 19 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 20 | github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= 21 | github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 22 | github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= 23 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 24 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 25 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 26 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 27 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 28 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 29 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 30 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 31 | github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= 32 | github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 33 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 34 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 35 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 36 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 37 | github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 38 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= 39 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 40 | github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= 41 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 42 | github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= 43 | github.com/klauspost/compress v1.13.6 h1:P76CopJELS0TiO2mebmnzgWaajssP/EszplttgQxcgc= 44 | github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= 45 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 46 | github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= 47 | github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= 48 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 49 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 50 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 51 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 52 | github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= 53 | github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= 54 | github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= 55 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 56 | github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= 57 | github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= 58 | github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= 59 | github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= 60 | github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= 61 | github.com/onsi/ginkgo/v2 v2.1.4/go.mod h1:um6tUpWM/cxCK3/FK8BXqEiUMUwRgSM4JXG47RKZmLU= 62 | github.com/onsi/ginkgo/v2 v2.1.6/go.mod h1:MEH45j8TBi6u9BMogfbp0stKC5cdGjumZj5Y7AG4VIk= 63 | github.com/onsi/ginkgo/v2 v2.3.0/go.mod h1:Eew0uilEqZmIEZr8JrvYlvOM7Rr6xzTmMV8AyFNU9d0= 64 | github.com/onsi/ginkgo/v2 v2.4.0/go.mod h1:iHkDK1fKGcBoEHT5W7YBq4RFWaQulw+caOMkAt4OrFo= 65 | github.com/onsi/ginkgo/v2 v2.5.0/go.mod h1:Luc4sArBICYCS8THh8v3i3i5CuSZO+RaQRaJoeNwomw= 66 | github.com/onsi/ginkgo/v2 v2.7.0 h1:/XxtEV3I3Eif/HobnVx9YmJgk8ENdRsuUmM+fLCFNow= 67 | github.com/onsi/ginkgo/v2 v2.7.0/go.mod h1:yjiuMwPokqY1XauOgju45q3sJt6VzQ/Fict1LFVcsAo= 68 | github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= 69 | github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= 70 | github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= 71 | github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= 72 | github.com/onsi/gomega v1.20.1/go.mod h1:DtrZpjmvpn2mPm4YWQa0/ALMDj9v4YxLgojwPeREyVo= 73 | github.com/onsi/gomega v1.21.1/go.mod h1:iYAIXgPSaDHak0LCMA+AWBpIKBr8WZicMxnE8luStNc= 74 | github.com/onsi/gomega v1.22.1/go.mod h1:x6n7VNe4hw0vkyYUM4mjIXx3JbLiPaBPNgB7PRQ1tuM= 75 | github.com/onsi/gomega v1.24.0/go.mod h1:Z/NWtiqwBrwUt4/2loMmHL63EDLnYHmVbuBpDr2vQAg= 76 | github.com/onsi/gomega v1.24.1/go.mod h1:3AOiACssS3/MajrniINInwbfOOtfZvplPzuRSmvt1jM= 77 | github.com/onsi/gomega v1.25.0 h1:Vw7br2PCDYijJHSfBOWhov+8cAnUf8MfMaIOV323l6Y= 78 | github.com/onsi/gomega v1.25.0/go.mod h1:r+zV744Re+DiYCIPRlYOTxn0YkOLcAnW8k1xXdMPGhM= 79 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 80 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 81 | github.com/redis/go-redis/v9 v9.0.5 h1:CuQcn5HIEeK7BgElubPP8CGtE0KakrnbBSTLjathl5o= 82 | github.com/redis/go-redis/v9 v9.0.5/go.mod h1:WqMKv5vnQbRuZstUwxQI195wHy+t4PuXDOjzMvcuQHk= 83 | github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= 84 | github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= 85 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 86 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 87 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 88 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 89 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 90 | github.com/vmihailenco/go-tinylfu v0.2.2 h1:H1eiG6HM36iniK6+21n9LLpzx1G9R3DJa2UjUjbynsI= 91 | github.com/vmihailenco/go-tinylfu v0.2.2/go.mod h1:CutYi2Q9puTxfcolkliPq4npPuofg9N9t8JVrjzwa3Q= 92 | github.com/vmihailenco/msgpack/v5 v5.3.4 h1:qMKAwOV+meBw2Y8k9cVwAy7qErtYCwBzZ2ellBfvnqc= 93 | github.com/vmihailenco/msgpack/v5 v5.3.4/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc= 94 | github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= 95 | github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= 96 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 97 | github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 98 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 99 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 100 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 101 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 102 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 103 | golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= 104 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 105 | golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= 106 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 107 | golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI= 108 | golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 109 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 110 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 111 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 112 | golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 113 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 114 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 115 | golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= 116 | golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 117 | golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= 118 | golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= 119 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 120 | golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= 121 | golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= 122 | golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= 123 | golang.org/x/net v0.5.0 h1:GyT4nK/YDHSqa1c4753ouYCDajOYKTja9Xb/OHtgvSw= 124 | golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= 125 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 126 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 127 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 128 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 129 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 130 | golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= 131 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 132 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 133 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 134 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 135 | golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 136 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 137 | golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 138 | golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 139 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 140 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 141 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 142 | golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 143 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 144 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 145 | golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 146 | golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 147 | golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 148 | golang.org/x/sys v0.0.0-20220422013727-9388b58f7150/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 149 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 150 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 151 | golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 152 | golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 153 | golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 154 | golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18= 155 | golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 156 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 157 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 158 | golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 159 | golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= 160 | golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= 161 | golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ= 162 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 163 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 164 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 165 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 166 | golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 167 | golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 168 | golang.org/x/text v0.6.0 h1:3XmdazWV+ubf7QgHSTWeykHOci5oeekaGJBLkrkaw4k= 169 | golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 170 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 171 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 172 | golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 173 | golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= 174 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 175 | golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA= 176 | golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ= 177 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 178 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 179 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 180 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 181 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 182 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 183 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 184 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 185 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 186 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 187 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 188 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 189 | google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= 190 | google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 191 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 192 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 193 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= 194 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 195 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 196 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 197 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 198 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 199 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 200 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 201 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 202 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 203 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 204 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 205 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 206 | -------------------------------------------------------------------------------- /local.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "math/rand" 5 | "sync" 6 | "time" 7 | 8 | "github.com/vmihailenco/go-tinylfu" 9 | ) 10 | 11 | type LocalCache interface { 12 | Set(key string, data []byte) 13 | Get(key string) ([]byte, bool) 14 | Del(key string) 15 | } 16 | 17 | type TinyLFU struct { 18 | mu sync.Mutex 19 | rand *rand.Rand 20 | lfu *tinylfu.T 21 | ttl time.Duration 22 | offset time.Duration 23 | } 24 | 25 | var _ LocalCache = (*TinyLFU)(nil) 26 | 27 | func NewTinyLFU(size int, ttl time.Duration) *TinyLFU { 28 | const maxOffset = 10 * time.Second 29 | 30 | offset := ttl / 10 31 | if offset > maxOffset { 32 | offset = maxOffset 33 | } 34 | 35 | return &TinyLFU{ 36 | rand: rand.New(rand.NewSource(time.Now().UnixNano())), 37 | lfu: tinylfu.New(size, 100000), 38 | ttl: ttl, 39 | offset: offset, 40 | } 41 | } 42 | 43 | func (c *TinyLFU) UseRandomizedTTL(offset time.Duration) { 44 | c.offset = offset 45 | } 46 | 47 | func (c *TinyLFU) Set(key string, b []byte) { 48 | c.mu.Lock() 49 | defer c.mu.Unlock() 50 | 51 | ttl := c.ttl 52 | if c.offset > 0 { 53 | ttl += time.Duration(c.rand.Int63n(int64(c.offset))) 54 | } 55 | 56 | c.lfu.Set(&tinylfu.Item{ 57 | Key: key, 58 | Value: b, 59 | ExpireAt: time.Now().Add(ttl), 60 | }) 61 | } 62 | 63 | func (c *TinyLFU) Get(key string) ([]byte, bool) { 64 | c.mu.Lock() 65 | defer c.mu.Unlock() 66 | 67 | val, ok := c.lfu.Get(key) 68 | if !ok { 69 | return nil, false 70 | } 71 | 72 | b := val.([]byte) 73 | return b, true 74 | } 75 | 76 | func (c *TinyLFU) Del(key string) { 77 | c.mu.Lock() 78 | defer c.mu.Unlock() 79 | 80 | c.lfu.Del(key) 81 | } 82 | -------------------------------------------------------------------------------- /local_test.go: -------------------------------------------------------------------------------- 1 | package cache_test 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "math/rand" 7 | "testing" 8 | "time" 9 | 10 | "github.com/go-redis/cache/v9" 11 | ) 12 | 13 | func TestTinyLFU_Get_CorruptionOnExpiry(t *testing.T) { 14 | strFor := func(i int) string { 15 | return fmt.Sprintf("a string %d", i) 16 | } 17 | keyName := func(i int) string { 18 | return fmt.Sprintf("key-%00000d", i) 19 | } 20 | 21 | mycache := cache.NewTinyLFU(1000, 1*time.Second) 22 | size := 50000 23 | // Put a bunch of stuff in the cache with a TTL of 1 second 24 | for i := 0; i < size; i++ { 25 | key := keyName(i) 26 | mycache.Set(key, []byte(strFor(i))) 27 | } 28 | 29 | // Read stuff for a bit longer than the TTL - that's when the corruption occurs 30 | ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) 31 | defer cancel() 32 | 33 | done := ctx.Done() 34 | loop: 35 | for { 36 | select { 37 | case <-done: 38 | // this is expected 39 | break loop 40 | default: 41 | i := rand.Intn(size) 42 | key := keyName(i) 43 | 44 | b, ok := mycache.Get(key) 45 | if !ok { 46 | continue loop 47 | } 48 | 49 | got := string(b) 50 | expected := strFor(i) 51 | if got != expected { 52 | t.Fatalf("expected=%q got=%q key=%q", expected, got, key) 53 | } 54 | } 55 | } 56 | } 57 | --------------------------------------------------------------------------------