├── .gitignore ├── LICENSE ├── README.md ├── assets └── cache.png ├── cache.go ├── cache_stress_test.go ├── cache_suite_test.go ├── cache_test.go ├── encoding.go ├── go.mod ├── go.sum ├── item.go ├── mem_cache.go ├── metrics.go ├── option.go └── redis_cache.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, build with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | .idea/ 15 | 16 | .git/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 seaguest 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cache 2 | 3 | This is a high-performance, lightweight distributed caching solution that implements the cache-aside pattern, built upon a combination of in-memory and Redis. The cache architecture includes a singular global Redis instance and multiple in-memory instances. Data changes can be synchronized across all in-memory cache instances depending on the cache update policy. 4 | 5 | The library's design gives priority to data retrieval from the in-memory cache first. If the data isn't found in the local memory cache, it then resorts to the Redis cache. Should the data be unavailable in both caches, the library invokes a loader function to fetch the data, storing it in the cache for future access, thus ensuring an always-on cache. 6 | 7 | ![alt text](./assets/cache.png "cache-aside pattern") 8 | 9 | ## Features 10 | 11 | - **Two-level cache** : in-memory cache first, redis-backed 12 | - **Easy to use** : simple api with minimum configuration. 13 | - **Data consistency** : all in-memory instances will be notified by `Pub-Sub` if any value gets updated, redis and in-memory will keep consistent (if cache update policy configured to UpdatePolicyBroadcast). 14 | - **Concurrency**: singleflight is used to avoid cache breakdown. 15 | - **Metrics** : provide callback function to measure the cache metrics. 16 | 17 | ## Sequence diagram 18 | 19 | ### cache get policy 20 | - GetPolicyReturnExpired: return found object even if it has expired. 21 | - GetPolicyReloadOnExpiry: reload object if found object has expired, then return. 22 | ### cache update policy 23 | - UpdatePolicyBroadcast: notify all cache instances if there is any data change. 24 | - UpdatePolicyNoBroadcast: don't notify all cache instances if there is any data change. 25 | 26 | The below sequence diagrams have GetPolicyReturnExpired + UpdatePolicyBroadcast. 27 | 28 | ### Reload from loader function 29 | 30 | ```mermaid 31 | sequenceDiagram 32 | participant APP as Application 33 | participant M as cache 34 | participant L as Local Cache 35 | participant L2 as Local Cache2 36 | participant S as Shared Cache 37 | participant R as LoadFunc(DB) 38 | 39 | APP ->> M: Cache.GetObject() 40 | alt reload 41 | M ->> R: LoadFunc 42 | R -->> M: return from LoadFunc 43 | M -->> APP: return 44 | M ->> S: redis.Set() 45 | M ->> L: notifyAll() 46 | M ->> L2: notifyAll() 47 | end 48 | ``` 49 | 50 | ### Cache GetObject 51 | 52 | ```mermaid 53 | sequenceDiagram 54 | participant APP as Application 55 | participant M as cache 56 | participant L as Local Cache 57 | participant L2 as Local Cache2 58 | participant S as Shared Cache 59 | participant R as LoadFunc(DB) 60 | 61 | APP ->> M: Cache.GetObject() 62 | alt Local Cache hit 63 | M ->> L: mem.Get() 64 | L -->> M: {interface{}, error} 65 | M -->> APP: return 66 | M -->> R: async reload if expired 67 | else Local Cache miss but Shared Cache hit 68 | M ->> L: mem.Get() 69 | L -->> M: cache miss 70 | M ->> S: redis.Get() 71 | S -->> M: {interface{}, error} 72 | M -->> APP: return 73 | M -->> R: async reload if expired 74 | else All miss 75 | M ->> L: mem.Get() 76 | L -->> M: cache miss 77 | M ->> S: redis.Get() 78 | S -->> M: cache miss 79 | M ->> R: sync reload 80 | R -->> M: return from reload 81 | M -->> APP: return 82 | end 83 | ``` 84 | 85 | ### Set 86 | 87 | ```mermaid 88 | sequenceDiagram 89 | participant APP as Application 90 | participant M as cache 91 | participant L as Local Cache 92 | participant L2 as Local Cache2 93 | participant S as Shared Cache 94 | 95 | APP ->> M: Cache.SetObject() 96 | alt Set 97 | M ->> S: redis.Set() 98 | M ->> L: notifyAll() 99 | M ->> L2: notifyAll() 100 | M -->> APP: return 101 | end 102 | ``` 103 | 104 | ### Delete 105 | 106 | ```mermaid 107 | sequenceDiagram 108 | participant APP as Application 109 | participant M as cache 110 | participant L as Local Cache 111 | participant L2 as Local Cache2 112 | participant S as Shared Cache 113 | 114 | APP ->> M: Cache.Delete() 115 | alt Delete 116 | M ->> S: redis.Delete() 117 | M ->> L: notifyAll() 118 | M ->> L2: notifyAll() 119 | M -->> APP: return 120 | end 121 | ``` 122 | 123 | ### Installation 124 | 125 | `go get -u github.com/seaguest/cache` 126 | 127 | ### API 128 | 129 | ```go 130 | type Cache interface { 131 | SetObject(ctx context.Context, key string, obj interface{}, ttl time.Duration, opts ...Option) error 132 | 133 | // GetObject loader function f() will be called in case cache all miss 134 | // suggest to use object_type#id as key or any other pattern which can easily extract object, aggregate metric for same object in onMetric 135 | GetObject(ctx context.Context, key string, obj interface{}, ttl time.Duration, f func() (interface{}, error), opts ...Option) error 136 | 137 | Delete(ctx context.Context, key string) error 138 | 139 | // Disable GetObject will call loader function in case cache is disabled. 140 | Disable() 141 | 142 | // DeleteFromMem allows to delete key from mem, for test purpose 143 | DeleteFromMem(key string) 144 | 145 | // DeleteFromRedis allows to delete key from redis, for test purpose 146 | DeleteFromRedis(key string) error 147 | 148 | } 149 | ``` 150 | 151 | ### Tips 152 | 153 | `github.com/seaguest/deepcopy`is adopted for deepcopy, returned value is deepcopied to avoid dirty data. 154 | please implement DeepCopy interface if you encounter deepcopy performance trouble. 155 | 156 | ```go 157 | func (p *TestStruct) DeepCopy() interface{} { 158 | c := *p 159 | return &c 160 | } 161 | ``` 162 | 163 | ### Usage 164 | 165 | ```go 166 | package main 167 | 168 | import ( 169 | "context" 170 | "fmt" 171 | "log" 172 | "time" 173 | 174 | "github.com/gomodule/redigo/redis" 175 | "github.com/seaguest/cache" 176 | ) 177 | 178 | type TestStruct struct { 179 | Name string 180 | } 181 | 182 | // this will be called by deepcopy to improves reflect copy performance 183 | func (p *TestStruct) DeepCopy() interface{} { 184 | c := *p 185 | return &c 186 | } 187 | 188 | func main() { 189 | pool := &redis.Pool{ 190 | MaxIdle: 1000, 191 | MaxActive: 1000, 192 | Wait: true, 193 | IdleTimeout: 240 * time.Second, 194 | TestOnBorrow: func(c redis.Conn, t time.Time) error { 195 | _, err := c.Do("PING") 196 | return err 197 | }, 198 | Dial: func() (redis.Conn, error) { 199 | return redis.Dial("tcp", "127.0.0.1:6379") 200 | }, 201 | } 202 | 203 | ehCache := cache.New( 204 | cache.GetConn(pool.Get), 205 | cache.GetPolicy(cache.GetPolicyReturnExpired), 206 | cache.UpdatePolicy(cache.UpdatePolicyNoBroadcast), 207 | cache.OnMetric(func(key string, metric string, elapsedTime time.Duration) { 208 | // handle metric 209 | }), 210 | cache.OnError(func(ctx context.Context, err error) { 211 | // handle error 212 | }), 213 | ) 214 | 215 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*2) 216 | defer cancel() 217 | 218 | var v TestStruct 219 | err := ehCache.GetObject(ctx, fmt.Sprintf("TestStruct:%d", 100), &v, time.Second*3, func() (interface{}, error) { 220 | // data fetch logic to be done here 221 | time.Sleep(time.Millisecond * 1200 * 1) 222 | return &TestStruct{Name: "test"}, nil 223 | }) 224 | log.Println(v, err) 225 | } 226 | 227 | 228 | ``` 229 | 230 | ### JetBrains 231 | 232 | Goland is an excellent IDE, thank JetBrains for their free Open Source licenses. 233 | -------------------------------------------------------------------------------- /assets/cache.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seaguest/cache/97cf4ed698814c0b2426473561ef52daaf977803/assets/cache.png -------------------------------------------------------------------------------- /cache.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "reflect" 7 | "strings" 8 | "sync" 9 | "time" 10 | 11 | "github.com/gomodule/redigo/redis" 12 | "github.com/pkg/errors" 13 | "github.com/seaguest/deepcopy" 14 | "golang.org/x/sync/singleflight" 15 | ) 16 | 17 | var ( 18 | ErrIllegalTTL = errors.New("illegal ttl, must be in whole numbers of seconds, no fractions") 19 | ) 20 | 21 | const ( 22 | defaultNamespace = "default" 23 | ) 24 | 25 | type Cache interface { 26 | SetObject(ctx context.Context, key string, obj interface{}, ttl time.Duration, opts ...Option) error 27 | 28 | // GetObject loader function f() will be called in case cache all miss 29 | // suggest to use object_type#id as key or any other pattern which can easily extract object, aggregate metric for same object in onMetric 30 | GetObject(ctx context.Context, key string, obj interface{}, ttl time.Duration, f func() (interface{}, error), opts ...Option) error 31 | 32 | Delete(ctx context.Context, key string) error 33 | 34 | // Disable GetObject will call loader function in case cache is disabled. 35 | Disable() 36 | 37 | // DeleteFromMem allows to delete key from mem, for test purpose 38 | DeleteFromMem(key string) 39 | 40 | // DeleteFromRedis allows to delete key from redis, for test purpose 41 | DeleteFromRedis(key string) error 42 | } 43 | 44 | type cache struct { 45 | options Options 46 | 47 | // store the pkg_path+type<->object mapping 48 | types sync.Map 49 | 50 | // rds cache, handles redis level cache 51 | rds *redisCache 52 | 53 | // mem cache, handles in-memory cache 54 | mem *memCache 55 | 56 | sfg singleflight.Group 57 | 58 | metric Metrics 59 | } 60 | 61 | func New(options ...Option) Cache { 62 | c := &cache{} 63 | opts := newOptions(options...) 64 | 65 | // set default namespace if missing 66 | if opts.Namespace == "" { 67 | opts.Namespace = defaultNamespace 68 | } 69 | 70 | // set separator 71 | if opts.Separator == "" { 72 | panic("Separator unspecified") 73 | } 74 | 75 | // set default RedisTTLFactor to 4 if missing 76 | if opts.RedisTTLFactor == 0 { 77 | opts.RedisTTLFactor = 4 78 | } 79 | 80 | // if get policy is not specified, use returnExpired policy, return data even if data is expired. 81 | if opts.GetPolicy == 0 { 82 | opts.GetPolicy = GetPolicyReturnExpired 83 | } 84 | // if update policy is not specified, use NoBroadcast policy, don't broadcast to other nodes when cache is updated. 85 | if opts.UpdatePolicy == 0 { 86 | opts.UpdatePolicy = UpdatePolicyNoBroadcast 87 | } 88 | 89 | // set default CleanInterval to 10s if missing 90 | if opts.CleanInterval == 0 { 91 | opts.CleanInterval = time.Second * 10 92 | } else if opts.CleanInterval < time.Second { 93 | panic("CleanInterval must be second at least") 94 | } 95 | 96 | if opts.OnError == nil { 97 | panic("OnError is nil") 98 | } 99 | 100 | c.options = opts 101 | c.metric = opts.Metric 102 | c.metric.namespace = opts.Namespace 103 | c.metric.separator = opts.Separator 104 | c.mem = newMemCache(opts.CleanInterval, c.metric) 105 | c.rds = newRedisCache(opts.GetConn, opts.RedisTTLFactor, c.metric) 106 | go c.watch() 107 | return c 108 | } 109 | 110 | // Disable , disable cache, call loader function for each call 111 | func (c *cache) Disable() { 112 | c.options.Disabled = true 113 | } 114 | 115 | func (c *cache) SetObject(ctx context.Context, key string, obj interface{}, ttl time.Duration, opts ...Option) error { 116 | opt := newOptions(opts...) 117 | done := make(chan error) 118 | var err error 119 | go func() { 120 | done <- c.setObject(ctx, key, obj, ttl, opt) 121 | }() 122 | 123 | select { 124 | case err = <-done: 125 | case <-ctx.Done(): 126 | err = errors.WithStack(ctx.Err()) 127 | } 128 | return err 129 | } 130 | 131 | func (c *cache) setObject(ctx context.Context, key string, obj interface{}, ttl time.Duration, opt Options) (err error) { 132 | if ttl > ttl.Truncate(time.Second) { 133 | return errors.WithStack(ErrIllegalTTL) 134 | } 135 | 136 | // use UpdateCachePolicy from inout if provided, otherwise take from global options. 137 | updatePolicy := opt.UpdatePolicy 138 | if updatePolicy == 0 { 139 | updatePolicy = c.options.UpdatePolicy 140 | } 141 | 142 | typeName := getTypeName(obj) 143 | c.checkType(typeName, obj, ttl) 144 | 145 | namespacedKey := c.namespacedKey(key) 146 | defer c.metric.Observe()(namespacedKey, MetricTypeSetCache, &err) 147 | 148 | _, err, _ = c.sfg.Do(namespacedKey+"_set", func() (interface{}, error) { 149 | // update local mem first 150 | c.mem.set(namespacedKey, newItem(obj, ttl)) 151 | 152 | _, err := c.rds.set(namespacedKey, obj, ttl) 153 | if err != nil { 154 | return nil, errors.WithStack(err) 155 | } 156 | 157 | c.notifyAll( 158 | ctx, 159 | &actionRequest{ 160 | Action: cacheSet, 161 | TypeName: typeName, 162 | Key: namespacedKey, 163 | Object: obj, 164 | UpdatePolicy: updatePolicy, 165 | }) 166 | return nil, nil 167 | }) 168 | return err 169 | } 170 | 171 | func (c *cache) GetObject(ctx context.Context, key string, obj interface{}, ttl time.Duration, f func() (interface{}, error), opts ...Option) error { 172 | opt := newOptions(opts...) 173 | 174 | // is disabled, call loader function 175 | if c.options.Disabled { 176 | o, err := f() 177 | if err != nil { 178 | return err 179 | } 180 | return c.copy(ctx, o, obj) 181 | } 182 | 183 | done := make(chan error) 184 | var err error 185 | go func() { 186 | done <- c.getObject(ctx, key, obj, ttl, f, opt) 187 | }() 188 | 189 | select { 190 | case err = <-done: 191 | case <-ctx.Done(): 192 | err = errors.WithStack(ctx.Err()) 193 | } 194 | return err 195 | } 196 | 197 | func (c *cache) getObject(ctx context.Context, key string, obj interface{}, ttl time.Duration, f func() (interface{}, error), opt Options) (err error) { 198 | if ttl > ttl.Truncate(time.Second) { 199 | return errors.WithStack(ErrIllegalTTL) 200 | } 201 | 202 | typeName := getTypeName(obj) 203 | c.checkType(typeName, obj, ttl) 204 | 205 | var expired bool 206 | namespacedKey := c.namespacedKey(key) 207 | defer c.metric.Observe()(namespacedKey, MetricTypeGetCache, &err) 208 | 209 | // use GetCachePolicy from inout if provided, otherwise take from global options. 210 | getPolicy := opt.GetPolicy 211 | if getPolicy == 0 { 212 | getPolicy = c.options.GetPolicy 213 | } 214 | 215 | var it *Item 216 | defer func() { 217 | if expired && getPolicy == GetPolicyReloadOnExpiry { 218 | it, err = c.resetObject(ctx, namespacedKey, ttl, f, opt) 219 | } 220 | // deepcopy before return 221 | if err == nil { 222 | err = c.copy(ctx, it.Object, obj) 223 | } 224 | 225 | // if expired and get policy is not ReloadOnExpiry, then do a async load. 226 | if expired && getPolicy != GetPolicyReloadOnExpiry { 227 | go func() { 228 | // async load metric 229 | defer c.metric.Observe()(namespacedKey, MetricTypeAsyncLoad, nil) 230 | 231 | _, resetErr := c.resetObject(ctx, namespacedKey, ttl, f, opt) 232 | if resetErr != nil { 233 | c.options.OnError(ctx, errors.WithStack(resetErr)) 234 | return 235 | } 236 | }() 237 | } 238 | }() 239 | 240 | // try to retrieve from local cache, return if found 241 | it = c.mem.get(namespacedKey) 242 | if it != nil { 243 | if it.Expired() { 244 | expired = true 245 | } 246 | return 247 | } 248 | 249 | var itf interface{} 250 | itf, err, _ = c.sfg.Do(namespacedKey+"_get", func() (interface{}, error) { 251 | // try to retrieve from redis, return if found 252 | v, redisErr := c.rds.get(namespacedKey, obj) 253 | if redisErr != nil { 254 | return nil, errors.WithStack(redisErr) 255 | } 256 | if v != nil { 257 | if v.Expired() { 258 | expired = true 259 | } else { 260 | // update memory cache since it is not previously found in mem 261 | c.mem.set(namespacedKey, v) 262 | } 263 | return v, nil 264 | } 265 | return c.resetObject(ctx, namespacedKey, ttl, f, opt) 266 | }) 267 | if err != nil { 268 | return 269 | } 270 | it = itf.(*Item) 271 | return 272 | } 273 | 274 | // resetObject load fresh data to redis and in-memory with loader function 275 | func (c *cache) resetObject(ctx context.Context, namespacedKey string, ttl time.Duration, f func() (interface{}, error), opt Options) (*Item, error) { 276 | itf, err, _ := c.sfg.Do(namespacedKey+"_reset", func() (it interface{}, err error) { 277 | // use UpdateCachePolicy from inout if provided, otherwise take from global options. 278 | updatePolicy := opt.UpdatePolicy 279 | if updatePolicy == 0 { 280 | updatePolicy = c.options.UpdatePolicy 281 | } 282 | 283 | // add metric for a fresh load 284 | defer c.metric.Observe()(namespacedKey, MetricTypeLoad, &err) 285 | 286 | defer func() { 287 | if r := recover(); r != nil { 288 | switch v := r.(type) { 289 | case error: 290 | err = errors.WithStack(v) 291 | default: 292 | err = errors.New(fmt.Sprint(r)) 293 | } 294 | c.options.OnError(ctx, err) 295 | } 296 | }() 297 | 298 | var o interface{} 299 | o, err = f() 300 | if err != nil { 301 | return 302 | } 303 | 304 | // update local mem first 305 | c.mem.set(namespacedKey, newItem(o, ttl)) 306 | 307 | it, err = c.rds.set(namespacedKey, o, ttl) 308 | if err != nil { 309 | return 310 | } 311 | 312 | // notifyAll 313 | c.notifyAll( 314 | ctx, 315 | &actionRequest{ 316 | Action: cacheSet, 317 | TypeName: getTypeName(o), 318 | Key: namespacedKey, 319 | Object: o, 320 | UpdatePolicy: updatePolicy, 321 | }) 322 | return 323 | }) 324 | if err != nil { 325 | return nil, err 326 | } 327 | return itf.(*Item), nil 328 | } 329 | 330 | func (c *cache) DeleteFromMem(key string) { 331 | namespacedKey := c.namespacedKey(key) 332 | c.mem.delete(namespacedKey) 333 | } 334 | 335 | func (c *cache) DeleteFromRedis(key string) error { 336 | namespacedKey := c.namespacedKey(key) 337 | return c.rds.delete(namespacedKey) 338 | } 339 | 340 | // Delete notify all cache instances to delete cache key 341 | func (c *cache) Delete(ctx context.Context, key string) (err error) { 342 | namespacedKey := c.namespacedKey(key) 343 | defer c.metric.Observe()(namespacedKey, MetricTypeDeleteCache, &err) 344 | 345 | // delete redis, then pub to delete cache 346 | if err = c.rds.delete(namespacedKey); err != nil { 347 | err = errors.WithStack(err) 348 | return 349 | } 350 | 351 | c.notifyAll( 352 | ctx, 353 | &actionRequest{ 354 | Action: cacheDelete, 355 | Key: namespacedKey, 356 | }) 357 | return 358 | } 359 | 360 | // checkType register type if not exists. 361 | func (c *cache) checkType(typeName string, obj interface{}, ttl time.Duration) { 362 | _, ok := c.types.Load(typeName) 363 | if !ok { 364 | c.types.Store(typeName, &objectType{typ: deepcopy.Copy(obj), ttl: ttl}) 365 | } 366 | } 367 | 368 | // copy object to return, to avoid dirty data 369 | func (c *cache) copy(ctx context.Context, src, dst interface{}) (err error) { 370 | defer func() { 371 | if r := recover(); r != nil { 372 | switch v := r.(type) { 373 | case error: 374 | err = errors.WithStack(v) 375 | default: 376 | err = errors.New(fmt.Sprint(r)) 377 | } 378 | c.options.OnError(ctx, err) 379 | } 380 | }() 381 | 382 | v := deepcopy.Copy(src) 383 | if reflect.ValueOf(v).IsValid() { 384 | reflect.ValueOf(dst).Elem().Set(reflect.Indirect(reflect.ValueOf(v))) 385 | } 386 | return 387 | } 388 | 389 | // get the global unique id for a given object type. 390 | func getTypeName(obj interface{}) string { 391 | return reflect.TypeOf(obj).Elem().PkgPath() + "/" + reflect.TypeOf(obj).String() 392 | } 393 | 394 | func (c *cache) namespacedKey(key string) string { 395 | return c.options.Namespace + ":" + key 396 | } 397 | 398 | type cacheAction int 399 | 400 | const ( 401 | cacheSet cacheAction = iota + 1 // cacheSet == 1 402 | cacheDelete 403 | ) 404 | 405 | // stores the object type and its ttl in memory 406 | type objectType struct { 407 | // object type 408 | typ interface{} 409 | 410 | // object ttl 411 | ttl time.Duration 412 | } 413 | 414 | // actionRequest defines an entity which will be broadcast to all cache instances 415 | type actionRequest struct { 416 | // cacheSet or cacheDelete 417 | Action cacheAction `json:"action"` 418 | 419 | // the type_name of the target object 420 | TypeName string `json:"type_name"` 421 | 422 | // key of the cache item 423 | Key string `json:"key"` 424 | 425 | // object stored in the cache, only used by caller, won't be broadcast 426 | Object interface{} `json:"-"` 427 | 428 | // the marshaled string of object 429 | Payload []byte `json:"payload"` 430 | 431 | // update policy, if NoBroadcast, then won't PUBLISH in set case. 432 | UpdatePolicy UpdateCachePolicy `json:"update_policy"` 433 | } 434 | 435 | func (c *cache) actionChannel() string { 436 | return c.options.Namespace + ":action_channel" 437 | } 438 | 439 | // watch the cache update 440 | func (c *cache) watch() { 441 | ctx := context.Background() 442 | begin: 443 | conn := c.options.GetConn() 444 | defer conn.Close() 445 | 446 | psc := redis.PubSubConn{Conn: conn} 447 | if err := psc.Subscribe(c.actionChannel()); err != nil { 448 | c.options.OnError(ctx, errors.WithStack(err)) 449 | return 450 | } 451 | 452 | for { 453 | switch v := psc.Receive().(type) { 454 | case redis.Message: 455 | var ar actionRequest 456 | if err := unmarshal(v.Data, &ar); err != nil { 457 | c.options.OnError(ctx, errors.WithStack(err)) 458 | continue 459 | } 460 | 461 | switch ar.Action { 462 | case cacheSet: 463 | objType, ok := c.types.Load(ar.TypeName) 464 | if !ok { 465 | continue 466 | } 467 | 468 | obj := deepcopy.Copy(objType.(*objectType).typ) 469 | if err := unmarshal(ar.Payload, obj); err != nil { 470 | c.options.OnError(ctx, errors.WithStack(err)) 471 | continue 472 | } 473 | 474 | it := newItem(obj, objType.(*objectType).ttl) 475 | it.Size = len(ar.Payload) 476 | c.mem.set(ar.Key, it) 477 | case cacheDelete: 478 | c.mem.delete(ar.Key) 479 | } 480 | case error: 481 | c.options.OnError(ctx, errors.WithStack(v)) 482 | time.Sleep(time.Second) // Wait for a second before attempting to receive messages again 483 | if strings.Contains(v.Error(), "use of closed network connection") || strings.Contains(v.Error(), "connect: connection refused") { 484 | // if connection becomes invalid, then restart watch with new conn 485 | goto begin 486 | } 487 | } 488 | } 489 | } 490 | 491 | // notifyAll will broadcast the cache change to all cache instances 492 | func (c *cache) notifyAll(ctx context.Context, ar *actionRequest) { 493 | // if update policy is NoBroadcast, don't broadcast for set. 494 | if ar.UpdatePolicy == UpdatePolicyNoBroadcast && ar.Action == cacheSet { 495 | return 496 | } 497 | 498 | bs, err := marshal(ar.Object) 499 | if err != nil { 500 | c.options.OnError(ctx, errors.WithStack(err)) 501 | return 502 | } 503 | ar.Payload = bs 504 | 505 | msgBody, err := marshal(ar) 506 | if err != nil { 507 | c.options.OnError(ctx, errors.WithStack(err)) 508 | return 509 | } 510 | 511 | conn := c.options.GetConn() 512 | defer conn.Close() 513 | 514 | _, err = conn.Do("PUBLISH", c.actionChannel(), string(msgBody)) 515 | if err != nil { 516 | c.options.OnError(ctx, errors.WithStack(err)) 517 | return 518 | } 519 | } 520 | -------------------------------------------------------------------------------- /cache_stress_test.go: -------------------------------------------------------------------------------- 1 | package cache_test 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "math/rand" 8 | "time" 9 | 10 | . "github.com/onsi/ginkgo/v2" 11 | . "github.com/onsi/gomega" 12 | "github.com/seaguest/cache" 13 | ) 14 | 15 | type ComplexStruct1 struct { 16 | ID int 17 | StrField string 18 | IntField int 19 | FloatField float64 20 | BoolField bool 21 | SliceField []string 22 | MapField map[string]int 23 | NestedStruct struct { 24 | StringField string 25 | IntField int 26 | } 27 | } 28 | 29 | type ComplexStruct2 struct { 30 | ID int 31 | StrField string 32 | IntField int 33 | FloatField float64 34 | BoolField bool 35 | SliceField []string 36 | MapField map[string]int 37 | NestedStruct struct { 38 | StringField string 39 | IntField int 40 | } 41 | } 42 | 43 | func (p *ComplexStruct1) DeepCopy() interface{} { 44 | c := *p 45 | if p.MapField != nil { 46 | c.MapField = make(map[string]int) 47 | for k, v := range p.MapField { 48 | c.MapField[k] = v 49 | } 50 | } 51 | return &c 52 | } 53 | 54 | func (p *ComplexStruct2) DeepCopy() interface{} { 55 | c := *p 56 | if p.MapField != nil { 57 | c.MapField = make(map[string]int) 58 | for k, v := range p.MapField { 59 | c.MapField[k] = v 60 | } 61 | } 62 | return &c 63 | } 64 | 65 | // Returns a random string of length 5 66 | func getRandomString() string { 67 | const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" 68 | b := make([]byte, 5) 69 | for i := range b { 70 | b[i] = charset[rand.Intn(len(charset))] 71 | } 72 | return string(b) 73 | } 74 | 75 | var _ = Describe("cache stress", func() { 76 | Context("Cache", func() { 77 | var ( 78 | cs1 ComplexStruct1 79 | cs2 ComplexStruct2 80 | ) 81 | 82 | BeforeEach(func() { 83 | cs1 = ComplexStruct1{ 84 | StrField: getRandomString(), 85 | IntField: rand.Intn(100), 86 | FloatField: rand.Float64() * 10, 87 | BoolField: rand.Intn(2) == 1, 88 | SliceField: []string{getRandomString(), getRandomString(), getRandomString()}, 89 | MapField: map[string]int{getRandomString(): rand.Intn(100), getRandomString(): rand.Intn(100)}, 90 | NestedStruct: struct { 91 | StringField string 92 | IntField int 93 | }{ 94 | StringField: getRandomString(), 95 | IntField: rand.Intn(100), 96 | }, 97 | } 98 | 99 | cs2 = ComplexStruct2{ 100 | StrField: getRandomString(), 101 | IntField: rand.Intn(100), 102 | FloatField: rand.Float64() * 10, 103 | BoolField: rand.Intn(2) == 1, 104 | SliceField: []string{getRandomString(), getRandomString(), getRandomString()}, 105 | MapField: map[string]int{getRandomString(): rand.Intn(100), getRandomString(): rand.Intn(100)}, 106 | NestedStruct: struct { 107 | StringField string 108 | IntField int 109 | }{ 110 | StringField: getRandomString(), 111 | IntField: rand.Intn(100), 112 | }, 113 | } 114 | }) 115 | 116 | Context("stress test", func() { 117 | bgCtx := context.Background() 118 | It("stress test", func() { 119 | mock := newMockCache("stress_test#1", time.Millisecond*1200, time.Second, false, cache.GetPolicyReloadOnExpiry, cache.UpdatePolicyNoBroadcast) 120 | 121 | for j := 0; j < 100; j++ { 122 | go func(id int) { 123 | for { 124 | ctx, _ := context.WithTimeout(bgCtx, time.Second*2) 125 | cs := cs1 126 | cs.ID = id 127 | 128 | var v ComplexStruct1 129 | err := mock.ehCache.GetObject(ctx, fmt.Sprintf("complex_struct_1#%d", id), &v, time.Second*3, func() (interface{}, error) { 130 | time.Sleep(time.Millisecond * 10) 131 | cs := cs1 132 | cs.ID = id 133 | return &cs, nil 134 | }) 135 | if err != nil { 136 | log.Println(err) 137 | } 138 | Ω(err).ToNot(HaveOccurred()) 139 | Ω(v).To(Equal(cs)) 140 | time.Sleep(time.Millisecond * 10) 141 | } 142 | }(j) 143 | } 144 | 145 | for j := 0; j < 100; j++ { 146 | go func(id int) { 147 | for { 148 | ctx, _ := context.WithTimeout(bgCtx, time.Second*2) 149 | cs := cs2 150 | cs.ID = id 151 | 152 | var v ComplexStruct2 153 | err := mock.ehCache.GetObject(ctx, fmt.Sprintf("complex_struct_2#%d", id), &v, time.Second*3, func() (interface{}, error) { 154 | time.Sleep(time.Millisecond * 10) 155 | cs := cs2 156 | cs.ID = id 157 | return &cs, nil 158 | }) 159 | if err != nil { 160 | log.Println(err) 161 | } 162 | Ω(err).ToNot(HaveOccurred()) 163 | Ω(v).To(Equal(cs)) 164 | time.Sleep(time.Millisecond * 10) 165 | } 166 | }(j) 167 | } 168 | 169 | time.Sleep(time.Second * 10) 170 | }) 171 | }) 172 | }) 173 | }) 174 | -------------------------------------------------------------------------------- /cache_suite_test.go: -------------------------------------------------------------------------------- 1 | package cache_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/ginkgo/v2" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestCache(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "Cache Suite") 13 | } 14 | -------------------------------------------------------------------------------- /cache_test.go: -------------------------------------------------------------------------------- 1 | package cache_test 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "log" 7 | "math" 8 | "time" 9 | 10 | "github.com/gomodule/redigo/redis" 11 | . "github.com/onsi/ginkgo/v2" 12 | . "github.com/onsi/gomega" 13 | "github.com/seaguest/cache" 14 | ) 15 | 16 | type TestStruct struct { 17 | Name string 18 | } 19 | 20 | type metric struct { 21 | Key string 22 | Type string 23 | ElapsedTime time.Duration 24 | } 25 | 26 | // this will be called by deepcopy to improves reflect copy performance 27 | func (p *TestStruct) DeepCopy() interface{} { 28 | c := *p 29 | return &c 30 | } 31 | 32 | type mockCache struct { 33 | ehCache cache.Cache 34 | metricChan chan metric 35 | val *TestStruct 36 | key string 37 | delay time.Duration 38 | } 39 | 40 | func newMockCache(key string, delay, ci time.Duration, checkMetric bool, getPolicy cache.GetCachePolicy, updatePolicy cache.UpdateCachePolicy) mockCache { 41 | mock := mockCache{} 42 | pool := &redis.Pool{ 43 | MaxIdle: 2, 44 | MaxActive: 5, 45 | Wait: true, 46 | IdleTimeout: 240 * time.Second, 47 | TestOnBorrow: func(c redis.Conn, t time.Time) error { 48 | _, err := c.Do("PING") 49 | return err 50 | }, 51 | Dial: func() (redis.Conn, error) { 52 | return redis.Dial("tcp", "127.0.0.1:7379") 53 | }, 54 | } 55 | metricChan := make(chan metric, 20) 56 | mock.ehCache = cache.New( 57 | cache.GetConn(pool.Get), 58 | cache.CleanInterval(ci), 59 | cache.Separator("#"), 60 | cache.GetPolicy(getPolicy), 61 | cache.UpdatePolicy(updatePolicy), 62 | cache.OnMetric(func(key, objectType string, metricType string, count int, elapsedTime time.Duration) { 63 | if metricType == cache.MetricTypeCount || metricType == cache.MetricTypeMemUsage { 64 | return 65 | } 66 | 67 | if !checkMetric { 68 | return 69 | } 70 | mc := metric{ 71 | Key: key, 72 | Type: metricType, 73 | ElapsedTime: elapsedTime, 74 | } 75 | metricChan <- mc 76 | }), 77 | cache.OnError(func(ctx context.Context, err error) { 78 | log.Printf("OnError:%+v", err) 79 | }), 80 | ) 81 | mock.metricChan = metricChan 82 | mock.key = key 83 | mock.val = &TestStruct{Name: "value for" + key} 84 | mock.delay = delay 85 | return mock 86 | } 87 | 88 | var _ = Describe("cache test", func() { 89 | log.SetFlags(log.LstdFlags | log.Lmicroseconds) 90 | 91 | Context("cache unit test", func() { 92 | Context("Test loadFunc", func() { 93 | It("loadFunc succeed", func() { 94 | mock := newMockCache("load_func_succeed#1", time.Millisecond*1200, time.Second, true, cache.GetPolicyReturnExpired, cache.UpdatePolicyBroadcast) 95 | mock.ehCache.DeleteFromRedis(mock.key) 96 | mock.ehCache.DeleteFromMem(mock.key) 97 | 98 | loadFunc := func() (interface{}, error) { 99 | time.Sleep(mock.delay) 100 | return mock.val, nil 101 | } 102 | 103 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) 104 | defer cancel() 105 | 106 | var v TestStruct 107 | err := mock.ehCache.GetObject(ctx, mock.key, &v, time.Second*3, loadFunc) 108 | Ω(err).ToNot(HaveOccurred()) 109 | Ω(&v).To(Equal(mock.val)) 110 | 111 | // make sure redis pub finished 112 | time.Sleep(time.Millisecond * 10) 113 | 114 | metricList := []string{cache.MetricTypeDeleteRedis, cache.MetricTypeDeleteMem, cache.MetricTypeGetMemMiss, cache.MetricTypeGetRedisMiss, cache.MetricTypeSetMem, cache.MetricTypeSetRedis, 115 | cache.MetricTypeLoad, cache.MetricTypeGetCache, cache.MetricTypeSetMem} 116 | 117 | for idx, metricType := range metricList { 118 | select { 119 | case mc := <-mock.metricChan: 120 | Ω(mc.Key).To(Equal(mock.key)) 121 | Ω(mc.Type).To(Equal(metricType)) 122 | if mc.Type == cache.MetricTypeLoad || (mc.Type == cache.MetricTypeGetCache && idx == 7) { 123 | // the first get_cache should be same as delay 124 | Ω(math.Abs(float64(mc.ElapsedTime-mock.delay)) < float64(time.Millisecond*10)).To(Equal(true)) 125 | } else { 126 | Ω(math.Abs(float64(mc.ElapsedTime)) < float64(time.Millisecond*10)).To(Equal(true)) 127 | } 128 | default: 129 | } 130 | } 131 | }) 132 | 133 | It("loadFunc error", func() { 134 | mock := newMockCache("load_func_error#1", time.Millisecond*1200, time.Second, true, cache.GetPolicyReturnExpired, cache.UpdatePolicyBroadcast) 135 | mock.ehCache.DeleteFromRedis(mock.key) 136 | mock.ehCache.DeleteFromMem(mock.key) 137 | 138 | unkownErr := errors.New("unknown error") 139 | loadFunc := func() (interface{}, error) { 140 | return nil, unkownErr 141 | } 142 | 143 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) 144 | defer cancel() 145 | 146 | var v TestStruct 147 | err := mock.ehCache.GetObject(ctx, mock.key, &v, time.Second*3, loadFunc) 148 | Ω(err).To(Equal(unkownErr)) 149 | 150 | metricList := []string{cache.MetricTypeDeleteRedis, cache.MetricTypeDeleteMem, cache.MetricTypeGetMemMiss, cache.MetricTypeGetRedisMiss} 151 | for _, metricType := range metricList { 152 | select { 153 | case mc := <-mock.metricChan: 154 | Ω(mc.Key).To(Equal(mock.key)) 155 | Ω(mc.Type).To(Equal(metricType)) 156 | Ω(math.Abs(float64(mc.ElapsedTime)) < float64(time.Millisecond*10)).To(Equal(true)) 157 | default: 158 | } 159 | } 160 | }) 161 | 162 | It("loadFunc panic string", func() { 163 | mock := newMockCache("load_func_panic_string#1", time.Millisecond*1200, time.Second, true, cache.GetPolicyReturnExpired, cache.UpdatePolicyBroadcast) 164 | mock.ehCache.DeleteFromRedis(mock.key) 165 | mock.ehCache.DeleteFromMem(mock.key) 166 | 167 | panicMsg := "panic string" 168 | loadFunc := func() (interface{}, error) { 169 | panic(panicMsg) 170 | return mock.val, nil 171 | } 172 | 173 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*2) 174 | defer cancel() 175 | 176 | var v TestStruct 177 | err := mock.ehCache.GetObject(ctx, mock.key, &v, time.Second*3, loadFunc) 178 | Ω(err.Error()).To(Equal(panicMsg)) 179 | 180 | metricList := []string{cache.MetricTypeDeleteRedis, cache.MetricTypeDeleteMem, cache.MetricTypeGetMemMiss, cache.MetricTypeGetRedisMiss} 181 | for _, metricType := range metricList { 182 | select { 183 | case mc := <-mock.metricChan: 184 | Ω(mc.Key).To(Equal(mock.key)) 185 | Ω(mc.Type).To(Equal(metricType)) 186 | Ω(math.Abs(float64(mc.ElapsedTime)) < float64(time.Millisecond*10)).To(Equal(true)) 187 | default: 188 | } 189 | } 190 | }) 191 | 192 | It("loadFunc panic error", func() { 193 | mock := newMockCache("load_func_panic_error#1", time.Millisecond*1200, time.Second, true, cache.GetPolicyReturnExpired, cache.UpdatePolicyBroadcast) 194 | mock.ehCache.DeleteFromRedis(mock.key) 195 | mock.ehCache.DeleteFromMem(mock.key) 196 | 197 | panicErr := errors.New("panic error") 198 | loadFunc := func() (interface{}, error) { 199 | panic(panicErr) 200 | return mock.val, nil 201 | } 202 | 203 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*2) 204 | defer cancel() 205 | 206 | var v TestStruct 207 | err := mock.ehCache.GetObject(ctx, mock.key, &v, time.Second*3, loadFunc) 208 | Ω(errors.Is(err, panicErr)).To(Equal(true)) 209 | 210 | metricList := []string{cache.MetricTypeDeleteRedis, cache.MetricTypeDeleteMem, cache.MetricTypeGetMemMiss, cache.MetricTypeGetRedisMiss} 211 | for _, metricType := range metricList { 212 | select { 213 | case mc := <-mock.metricChan: 214 | Ω(mc.Key).To(Equal(mock.key)) 215 | Ω(mc.Type).To(Equal(metricType)) 216 | Ω(math.Abs(float64(mc.ElapsedTime)) < float64(time.Millisecond*10)).To(Equal(true)) 217 | default: 218 | } 219 | } 220 | }) 221 | 222 | It("loadFunc timeout", func() { 223 | mock := newMockCache("load_func_panic_timeout#1", time.Millisecond*1200, time.Second, true, cache.GetPolicyReturnExpired, cache.UpdatePolicyBroadcast) 224 | mock.ehCache.DeleteFromRedis(mock.key) 225 | mock.ehCache.DeleteFromMem(mock.key) 226 | 227 | loadFunc := func() (interface{}, error) { 228 | time.Sleep(mock.delay) 229 | return mock.val, nil 230 | } 231 | 232 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*1) 233 | defer cancel() 234 | 235 | var v TestStruct 236 | err := mock.ehCache.GetObject(ctx, mock.key, &v, time.Second*3, loadFunc) 237 | Ω(err).To(MatchError(context.DeadlineExceeded)) 238 | 239 | // must wait goroutine to exit successfully, otherwise the other test will receive the redis-pub notification 240 | time.Sleep(time.Millisecond * 300) 241 | 242 | metricList := []string{cache.MetricTypeDeleteRedis, cache.MetricTypeDeleteMem, cache.MetricTypeGetMemMiss, cache.MetricTypeGetRedisMiss} 243 | for _, metricType := range metricList { 244 | select { 245 | case mc := <-mock.metricChan: 246 | Ω(mc.Key).To(Equal(mock.key)) 247 | Ω(mc.Type).To(Equal(metricType)) 248 | Ω(math.Abs(float64(mc.ElapsedTime)) < float64(time.Millisecond*10)).To(Equal(true)) 249 | default: 250 | } 251 | } 252 | }) 253 | }) 254 | 255 | Context("Test redis hit", func() { 256 | It("redis hit ok", func() { 257 | mock := newMockCache("redis_hit_ok#1", time.Millisecond*1200, time.Second, true, cache.GetPolicyReturnExpired, cache.UpdatePolicyBroadcast) 258 | mock.ehCache.DeleteFromRedis(mock.key) 259 | mock.ehCache.DeleteFromMem(mock.key) 260 | 261 | loadFunc := func() (interface{}, error) { 262 | time.Sleep(mock.delay) 263 | return mock.val, nil 264 | } 265 | 266 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) 267 | defer cancel() 268 | 269 | var v TestStruct 270 | err := mock.ehCache.GetObject(ctx, mock.key, &v, time.Second*1, loadFunc) 271 | Ω(err).ToNot(HaveOccurred()) 272 | Ω(&v).To(Equal(mock.val)) 273 | 274 | // make sure redis pub finished, mem get updated 275 | time.Sleep(time.Millisecond * 10) 276 | mock.ehCache.DeleteFromMem(mock.key) 277 | 278 | ctx, cancel = context.WithTimeout(context.Background(), time.Second*5) 279 | defer cancel() 280 | err = mock.ehCache.GetObject(ctx, mock.key, &v, time.Second*1, loadFunc) 281 | Ω(err).ToNot(HaveOccurred()) 282 | Ω(&v).To(Equal(mock.val)) 283 | 284 | // make sure redis pub finished, mem get updated 285 | time.Sleep(time.Millisecond * 10) 286 | 287 | metricList := []string{cache.MetricTypeDeleteRedis, cache.MetricTypeDeleteMem, cache.MetricTypeGetMemMiss, cache.MetricTypeGetRedisMiss, 288 | cache.MetricTypeSetMem, cache.MetricTypeSetRedis, cache.MetricTypeLoad, cache.MetricTypeGetCache, cache.MetricTypeSetMem, cache.MetricTypeDeleteMem, cache.MetricTypeGetMemMiss, 289 | cache.MetricTypeGetRedisHit, cache.MetricTypeSetMem, cache.MetricTypeGetCache, 290 | } 291 | for idx, metricType := range metricList { 292 | select { 293 | case mc := <-mock.metricChan: 294 | Ω(mc.Key).To(Equal(mock.key)) 295 | Ω(mc.Type).To(Equal(metricType)) 296 | if mc.Type == cache.MetricTypeLoad || (mc.Type == cache.MetricTypeGetCache && idx == 7) { 297 | // the first get_cache should be same as delay 298 | Ω(math.Abs(float64(mc.ElapsedTime-mock.delay)) < float64(time.Millisecond*10)).To(Equal(true)) 299 | } else { 300 | Ω(math.Abs(float64(mc.ElapsedTime)) < float64(time.Millisecond*10)).To(Equal(true)) 301 | } 302 | default: 303 | } 304 | } 305 | }) 306 | }) 307 | 308 | It("redis hit expired return", func() { 309 | mock := newMockCache("redis_hit_expired#1", time.Millisecond*1200, time.Second, true, cache.GetPolicyReturnExpired, cache.UpdatePolicyBroadcast) 310 | mock.ehCache.DeleteFromRedis(mock.key) 311 | mock.ehCache.DeleteFromMem(mock.key) 312 | 313 | loadFunc := func() (interface{}, error) { 314 | time.Sleep(mock.delay) 315 | return mock.val, nil 316 | } 317 | 318 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) 319 | defer cancel() 320 | 321 | var v TestStruct 322 | err := mock.ehCache.GetObject(ctx, mock.key, &v, time.Second*1, loadFunc) 323 | Ω(err).ToNot(HaveOccurred()) 324 | Ω(&v).To(Equal(mock.val)) 325 | 326 | // wait redis expired 327 | time.Sleep(time.Millisecond * 1010) 328 | mock.ehCache.DeleteFromMem(mock.key) 329 | 330 | ctx, cancel = context.WithTimeout(context.Background(), time.Second*5) 331 | defer cancel() 332 | err = mock.ehCache.GetObject(ctx, mock.key, &v, time.Second*1, loadFunc) 333 | Ω(err).ToNot(HaveOccurred()) 334 | Ω(&v).To(Equal(mock.val)) 335 | 336 | // make sure redis pub finished, mem get updated 337 | time.Sleep(mock.delay + time.Millisecond*10) 338 | 339 | metricList := []string{cache.MetricTypeDeleteRedis, cache.MetricTypeDeleteMem, cache.MetricTypeGetMemMiss, cache.MetricTypeGetRedisMiss, 340 | cache.MetricTypeSetMem, cache.MetricTypeSetRedis, cache.MetricTypeLoad, cache.MetricTypeGetCache, cache.MetricTypeSetMem, cache.MetricTypeDeleteMem, cache.MetricTypeGetMemMiss, 341 | cache.MetricTypeGetRedisExpired, cache.MetricTypeGetCache, cache.MetricTypeSetMem, cache.MetricTypeSetRedis, cache.MetricTypeLoad, cache.MetricTypeAsyncLoad, cache.MetricTypeSetMem, 342 | } 343 | 344 | for idx, metricType := range metricList { 345 | select { 346 | case mc := <-mock.metricChan: 347 | Ω(mc.Key).To(Equal(mock.key)) 348 | Ω(mc.Type).To(Equal(metricType)) 349 | if mc.Type == cache.MetricTypeLoad || mc.Type == cache.MetricTypeAsyncLoad || (mc.Type == cache.MetricTypeGetCache && idx == 7) { 350 | // the first get_cache should be same as delay 351 | Ω(math.Abs(float64(mc.ElapsedTime-mock.delay)) < float64(time.Millisecond*10)).To(Equal(true)) 352 | } else { 353 | Ω(math.Abs(float64(mc.ElapsedTime)) < float64(time.Millisecond*10)).To(Equal(true)) 354 | } 355 | default: 356 | } 357 | } 358 | }) 359 | 360 | Context("Test mem hit", func() { 361 | It("mem hit ok", func() { 362 | mock := newMockCache("mem_hit_ok#1", time.Millisecond*1200, time.Second*1, true, cache.GetPolicyReturnExpired, cache.UpdatePolicyBroadcast) 363 | mock.ehCache.DeleteFromRedis(mock.key) 364 | mock.ehCache.DeleteFromMem(mock.key) 365 | 366 | loadFunc := func() (interface{}, error) { 367 | time.Sleep(mock.delay) 368 | return mock.val, nil 369 | } 370 | 371 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) 372 | defer cancel() 373 | 374 | var v TestStruct 375 | err := mock.ehCache.GetObject(ctx, mock.key, &v, time.Second*3, loadFunc) 376 | Ω(err).ToNot(HaveOccurred()) 377 | Ω(&v).To(Equal(mock.val)) 378 | 379 | // make sure redis pub finished 380 | time.Sleep(time.Millisecond * 10) 381 | 382 | ctx, cancel = context.WithTimeout(context.Background(), time.Second*5) 383 | defer cancel() 384 | err = mock.ehCache.GetObject(ctx, mock.key, &v, time.Second*1, loadFunc) 385 | Ω(err).ToNot(HaveOccurred()) 386 | Ω(&v).To(Equal(mock.val)) 387 | 388 | metricList := []string{cache.MetricTypeDeleteRedis, cache.MetricTypeDeleteMem, cache.MetricTypeGetMemMiss, cache.MetricTypeGetRedisMiss, 389 | cache.MetricTypeSetMem, cache.MetricTypeSetRedis, cache.MetricTypeLoad, cache.MetricTypeGetCache, cache.MetricTypeSetMem, cache.MetricTypeGetMemHit, cache.MetricTypeGetCache, 390 | } 391 | 392 | for idx, metricType := range metricList { 393 | select { 394 | case mc := <-mock.metricChan: 395 | Ω(mc.Key).To(Equal(mock.key)) 396 | Ω(mc.Type).To(Equal(metricType)) 397 | if mc.Type == cache.MetricTypeLoad || (mc.Type == cache.MetricTypeGetCache && idx == 7) { 398 | // the first get_cache should be same as delay 399 | Ω(math.Abs(float64(mc.ElapsedTime-mock.delay)) < float64(time.Millisecond*10)).To(Equal(true)) 400 | } else { 401 | Ω(math.Abs(float64(mc.ElapsedTime)) < float64(time.Millisecond*10)).To(Equal(true)) 402 | } 403 | default: 404 | } 405 | } 406 | }) 407 | 408 | It("mem hit expired return", func() { 409 | mock := newMockCache("mem_hit_expired#1", time.Millisecond*1200, time.Second*5, true, cache.GetPolicyReturnExpired, cache.UpdatePolicyBroadcast) 410 | mock.ehCache.DeleteFromRedis(mock.key) 411 | mock.ehCache.DeleteFromMem(mock.key) 412 | 413 | loadFunc := func() (interface{}, error) { 414 | time.Sleep(mock.delay) 415 | return mock.val, nil 416 | } 417 | 418 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) 419 | defer cancel() 420 | 421 | var v TestStruct 422 | err := mock.ehCache.GetObject(ctx, mock.key, &v, time.Second*1, loadFunc) 423 | Ω(err).ToNot(HaveOccurred()) 424 | Ω(&v).To(Equal(mock.val)) 425 | 426 | // wait 1100 ms to expire mem 427 | time.Sleep(time.Millisecond * 1100) 428 | 429 | ctx, cancel = context.WithTimeout(context.Background(), time.Second*5) 430 | defer cancel() 431 | err = mock.ehCache.GetObject(ctx, mock.key, &v, time.Second*2, loadFunc) 432 | Ω(err).ToNot(HaveOccurred()) 433 | Ω(&v).To(Equal(mock.val)) 434 | 435 | // wait last async load finish 436 | time.Sleep(mock.delay + time.Millisecond*10) 437 | 438 | metricList := []string{cache.MetricTypeDeleteRedis, cache.MetricTypeDeleteMem, cache.MetricTypeGetMemMiss, cache.MetricTypeGetRedisMiss, 439 | cache.MetricTypeSetMem, cache.MetricTypeSetRedis, cache.MetricTypeLoad, cache.MetricTypeGetCache, cache.MetricTypeSetMem, cache.MetricTypeGetMemExpired, cache.MetricTypeGetCache, 440 | cache.MetricTypeSetMem, cache.MetricTypeSetRedis, cache.MetricTypeLoad, cache.MetricTypeAsyncLoad, cache.MetricTypeSetMem, 441 | } 442 | 443 | for idx, metricType := range metricList { 444 | select { 445 | case mc := <-mock.metricChan: 446 | Ω(mc.Key).To(Equal(mock.key)) 447 | Ω(mc.Type).To(Equal(metricType)) 448 | if mc.Type == cache.MetricTypeLoad || mc.Type == cache.MetricTypeAsyncLoad || (mc.Type == cache.MetricTypeGetCache && idx == 7) { 449 | // the first get_cache should be same as delay 450 | Ω(math.Abs(float64(mc.ElapsedTime-mock.delay)) < float64(time.Millisecond*10)).To(Equal(true)) 451 | } else { 452 | Ω(math.Abs(float64(mc.ElapsedTime)) < float64(time.Millisecond*10)).To(Equal(true)) 453 | } 454 | default: 455 | } 456 | } 457 | }) 458 | 459 | It("mem hit expired option reload", func() { 460 | mock := newMockCache("mem_hit_expired#1", time.Millisecond*1200, time.Second*5, true, cache.GetPolicyReturnExpired, cache.UpdatePolicyBroadcast) 461 | mock.ehCache.DeleteFromRedis(mock.key) 462 | mock.ehCache.DeleteFromMem(mock.key) 463 | 464 | loadFunc := func() (interface{}, error) { 465 | time.Sleep(mock.delay) 466 | return mock.val, nil 467 | } 468 | 469 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) 470 | defer cancel() 471 | 472 | var v TestStruct 473 | err := mock.ehCache.GetObject(ctx, mock.key, &v, time.Second*1, loadFunc) 474 | Ω(err).ToNot(HaveOccurred()) 475 | Ω(&v).To(Equal(mock.val)) 476 | 477 | // wait 1100 ms to expire mem 478 | time.Sleep(time.Millisecond * 1100) 479 | 480 | ctx, cancel = context.WithTimeout(context.Background(), time.Second*5) 481 | defer cancel() 482 | err = mock.ehCache.GetObject(ctx, mock.key, &v, time.Second*2, loadFunc, cache.GetPolicy(cache.GetPolicyReloadOnExpiry)) 483 | Ω(err).ToNot(HaveOccurred()) 484 | Ω(&v).To(Equal(mock.val)) 485 | 486 | // wait last async load finish 487 | time.Sleep(mock.delay + time.Millisecond*10) 488 | 489 | metricList := []string{cache.MetricTypeDeleteRedis, cache.MetricTypeDeleteMem, cache.MetricTypeGetMemMiss, cache.MetricTypeGetRedisMiss, 490 | cache.MetricTypeSetMem, cache.MetricTypeSetRedis, cache.MetricTypeLoad, cache.MetricTypeGetCache, cache.MetricTypeSetMem, cache.MetricTypeGetMemExpired, 491 | cache.MetricTypeSetMem, cache.MetricTypeSetRedis, cache.MetricTypeLoad, cache.MetricTypeGetCache, 492 | } 493 | 494 | for idx, metricType := range metricList { 495 | select { 496 | case mc := <-mock.metricChan: 497 | Ω(mc.Key).To(Equal(mock.key)) 498 | Ω(mc.Type).To(Equal(metricType)) 499 | if mc.Type == cache.MetricTypeLoad || mc.Type == cache.MetricTypeAsyncLoad || (mc.Type == cache.MetricTypeGetCache && (idx == 7 || idx == 13)) { 500 | // the first get_cache should be same as delay 501 | Ω(math.Abs(float64(mc.ElapsedTime-mock.delay)) < float64(time.Millisecond*10)).To(Equal(true)) 502 | } else { 503 | Ω(math.Abs(float64(mc.ElapsedTime)) < float64(time.Millisecond*10)).To(Equal(true)) 504 | } 505 | default: 506 | } 507 | } 508 | }) 509 | }) 510 | 511 | Context("Test delete", func() { 512 | It("delete ok", func() { 513 | mock := newMockCache("delete_ok#1", time.Millisecond*1200, time.Second*1, true, cache.GetPolicyReturnExpired, cache.UpdatePolicyBroadcast) 514 | mock.ehCache.DeleteFromRedis(mock.key) 515 | mock.ehCache.DeleteFromMem(mock.key) 516 | mock.ehCache.Delete(context.Background(), mock.key) 517 | 518 | // wait redis-pub received 519 | time.Sleep(time.Millisecond * 10) 520 | 521 | metricList := []string{cache.MetricTypeDeleteRedis, cache.MetricTypeDeleteMem, cache.MetricTypeDeleteRedis, cache.MetricTypeDeleteCache, cache.MetricTypeDeleteMem} 522 | 523 | for _, metricType := range metricList { 524 | select { 525 | case mc := <-mock.metricChan: 526 | Ω(mc.Key).To(Equal(mock.key)) 527 | Ω(mc.Type).To(Equal(metricType)) 528 | Ω(math.Abs(float64(mc.ElapsedTime)) < float64(time.Millisecond*10)).To(Equal(true)) 529 | default: 530 | } 531 | } 532 | }) 533 | }) 534 | 535 | Context("Test setobject", func() { 536 | It("setobject ok", func() { 537 | mock := newMockCache("set_object_ok#1", time.Millisecond*1200, time.Second*1, true, cache.GetPolicyReturnExpired, cache.UpdatePolicyBroadcast) 538 | mock.ehCache.DeleteFromRedis(mock.key) 539 | mock.ehCache.DeleteFromMem(mock.key) 540 | 541 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) 542 | defer cancel() 543 | err := mock.ehCache.SetObject(ctx, mock.key, mock.val, time.Second) 544 | Ω(err).ToNot(HaveOccurred()) 545 | 546 | // wait redis-pub received 547 | time.Sleep(time.Millisecond * 10) 548 | 549 | metricList := []string{cache.MetricTypeDeleteRedis, cache.MetricTypeDeleteMem, cache.MetricTypeSetMem, cache.MetricTypeSetRedis, cache.MetricTypeSetCache, cache.MetricTypeSetMem} 550 | 551 | for _, metricType := range metricList { 552 | select { 553 | case mc := <-mock.metricChan: 554 | Ω(mc.Key).To(Equal(mock.key)) 555 | Ω(mc.Type).To(Equal(metricType)) 556 | Ω(math.Abs(float64(mc.ElapsedTime)) < float64(time.Millisecond*10)).To(Equal(true)) 557 | default: 558 | } 559 | } 560 | }) 561 | }) 562 | 563 | Context("Test hit EXPIRED with reloadOnExpiry", func() { 564 | It("mem hit expired reload", func() { 565 | mock := newMockCache("mem_hit_expired_reload#1", time.Millisecond*1200, time.Second*5, true, cache.GetPolicyReloadOnExpiry, cache.UpdatePolicyNoBroadcast) 566 | mock.ehCache.DeleteFromRedis(mock.key) 567 | mock.ehCache.DeleteFromMem(mock.key) 568 | 569 | loadFunc := func() (interface{}, error) { 570 | time.Sleep(mock.delay) 571 | return mock.val, nil 572 | } 573 | 574 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) 575 | defer cancel() 576 | 577 | var v TestStruct 578 | err := mock.ehCache.GetObject(ctx, mock.key, &v, time.Second*1, loadFunc) 579 | Ω(err).ToNot(HaveOccurred()) 580 | Ω(&v).To(Equal(mock.val)) 581 | 582 | // wait 1100 ms to expire mem 583 | time.Sleep(time.Millisecond * 1100) 584 | 585 | ctx, cancel = context.WithTimeout(context.Background(), time.Second*5) 586 | defer cancel() 587 | err = mock.ehCache.GetObject(ctx, mock.key, &v, time.Second*2, loadFunc) 588 | Ω(err).ToNot(HaveOccurred()) 589 | Ω(&v).To(Equal(mock.val)) 590 | 591 | // wait last async load finish 592 | time.Sleep(mock.delay + time.Millisecond*10) 593 | 594 | metricList := []string{cache.MetricTypeDeleteRedis, cache.MetricTypeDeleteMem, cache.MetricTypeGetMemMiss, cache.MetricTypeGetRedisMiss, 595 | cache.MetricTypeSetMem, cache.MetricTypeSetRedis, cache.MetricTypeLoad, cache.MetricTypeGetCache, cache.MetricTypeGetMemExpired, 596 | cache.MetricTypeSetMem, cache.MetricTypeSetRedis, cache.MetricTypeLoad, cache.MetricTypeGetCache, 597 | } 598 | for idx, metricType := range metricList { 599 | select { 600 | case mc := <-mock.metricChan: 601 | Ω(mc.Key).To(Equal(mock.key)) 602 | Ω(mc.Type).To(Equal(metricType)) 603 | if mc.Type == cache.MetricTypeLoad || mc.Type == cache.MetricTypeAsyncLoad || (mc.Type == cache.MetricTypeGetCache && (idx == 7 || idx == 12)) { 604 | // the first get_cache should be same as delay 605 | Ω(math.Abs(float64(mc.ElapsedTime-mock.delay)) < float64(time.Millisecond*10)).To(Equal(true)) 606 | } else { 607 | Ω(math.Abs(float64(mc.ElapsedTime)) < float64(time.Millisecond*10)).To(Equal(true)) 608 | } 609 | default: 610 | } 611 | } 612 | }) 613 | }) 614 | 615 | It("redis hit expired reload", func() { 616 | mock := newMockCache("redis_hit_expired_reload#1", time.Millisecond*1200, time.Second, true, cache.GetPolicyReloadOnExpiry, cache.UpdatePolicyNoBroadcast) 617 | mock.ehCache.DeleteFromRedis(mock.key) 618 | mock.ehCache.DeleteFromMem(mock.key) 619 | 620 | loadFunc := func() (interface{}, error) { 621 | time.Sleep(mock.delay) 622 | return mock.val, nil 623 | } 624 | 625 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) 626 | defer cancel() 627 | 628 | var v TestStruct 629 | err := mock.ehCache.GetObject(ctx, mock.key, &v, time.Second*1, loadFunc) 630 | Ω(err).ToNot(HaveOccurred()) 631 | Ω(&v).To(Equal(mock.val)) 632 | 633 | // wait redis expired 634 | time.Sleep(time.Millisecond * 1010) 635 | mock.ehCache.DeleteFromMem(mock.key) 636 | 637 | ctx, cancel = context.WithTimeout(context.Background(), time.Second*5) 638 | defer cancel() 639 | err = mock.ehCache.GetObject(ctx, mock.key, &v, time.Second*1, loadFunc) 640 | Ω(err).ToNot(HaveOccurred()) 641 | Ω(&v).To(Equal(mock.val)) 642 | 643 | // make sure redis pub finished, mem get updated 644 | time.Sleep(mock.delay + time.Millisecond*10) 645 | 646 | metricList := []string{cache.MetricTypeDeleteRedis, cache.MetricTypeDeleteMem, cache.MetricTypeGetMemMiss, cache.MetricTypeGetRedisMiss, 647 | cache.MetricTypeSetMem, cache.MetricTypeSetRedis, cache.MetricTypeLoad, cache.MetricTypeGetCache, cache.MetricTypeDeleteMem, cache.MetricTypeGetMemMiss, 648 | cache.MetricTypeGetRedisExpired, cache.MetricTypeSetMem, cache.MetricTypeSetRedis, cache.MetricTypeLoad, cache.MetricTypeGetCache, 649 | } 650 | 651 | for idx, metricType := range metricList { 652 | select { 653 | case mc := <-mock.metricChan: 654 | Ω(mc.Key).To(Equal(mock.key)) 655 | Ω(mc.Type).To(Equal(metricType)) 656 | if mc.Type == cache.MetricTypeLoad || mc.Type == cache.MetricTypeAsyncLoad || (mc.Type == cache.MetricTypeGetCache && (idx == 7 || idx == 14)) { 657 | // the first get_cache should be same as delay 658 | Ω(math.Abs(float64(mc.ElapsedTime-mock.delay)) < float64(time.Millisecond*10)).To(Equal(true)) 659 | } else { 660 | Ω(math.Abs(float64(mc.ElapsedTime)) < float64(time.Millisecond*10)).To(Equal(true)) 661 | } 662 | default: 663 | } 664 | } 665 | }) 666 | 667 | }) 668 | }) 669 | -------------------------------------------------------------------------------- /encoding.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "google.golang.org/protobuf/encoding/protojson" 7 | "google.golang.org/protobuf/proto" 8 | ) 9 | 10 | func marshal(v interface{}) ([]byte, error) { 11 | protoVal, ok := v.(proto.Message) 12 | if ok { 13 | return protojson.Marshal(protoVal) 14 | } else { 15 | return json.Marshal(v) 16 | } 17 | } 18 | 19 | func unmarshal(data []byte, v interface{}) error { 20 | protoVal, ok := v.(proto.Message) 21 | if ok { 22 | return protojson.Unmarshal(data, protoVal) 23 | } else { 24 | return json.Unmarshal(data, v) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/seaguest/cache 2 | 3 | go 1.15 4 | 5 | require ( 6 | github.com/gomodule/redigo v1.8.9 7 | github.com/onsi/ginkgo/v2 v2.9.2 8 | github.com/onsi/gomega v1.27.6 9 | github.com/pkg/errors v0.9.1 10 | github.com/seaguest/deepcopy v1.0.0 11 | golang.org/x/sync v0.1.0 12 | google.golang.org/protobuf v1.28.0 13 | ) 14 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= 2 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= 3 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 4 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 6 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 8 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 9 | github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= 10 | github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 11 | github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= 12 | github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= 13 | github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= 14 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 15 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 16 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 17 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 18 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 19 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 20 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 21 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 22 | github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 23 | github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= 24 | github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 25 | github.com/gomodule/redigo v1.8.9 h1:Sl3u+2BI/kk+VEatbj0scLdrFhjPmbxOc1myhDP41ws= 26 | github.com/gomodule/redigo v1.8.9/go.mod h1:7ArFNvsTjH8GMMzB4uy1snslv2BwmginuMs06a1uzZE= 27 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 28 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 29 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 30 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 31 | github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 32 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= 33 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 34 | github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE= 35 | github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= 36 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 37 | github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= 38 | github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= 39 | github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= 40 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 41 | github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= 42 | github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= 43 | github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= 44 | github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= 45 | github.com/onsi/ginkgo/v2 v2.1.4/go.mod h1:um6tUpWM/cxCK3/FK8BXqEiUMUwRgSM4JXG47RKZmLU= 46 | github.com/onsi/ginkgo/v2 v2.1.6/go.mod h1:MEH45j8TBi6u9BMogfbp0stKC5cdGjumZj5Y7AG4VIk= 47 | github.com/onsi/ginkgo/v2 v2.3.0/go.mod h1:Eew0uilEqZmIEZr8JrvYlvOM7Rr6xzTmMV8AyFNU9d0= 48 | github.com/onsi/ginkgo/v2 v2.4.0/go.mod h1:iHkDK1fKGcBoEHT5W7YBq4RFWaQulw+caOMkAt4OrFo= 49 | github.com/onsi/ginkgo/v2 v2.5.0/go.mod h1:Luc4sArBICYCS8THh8v3i3i5CuSZO+RaQRaJoeNwomw= 50 | github.com/onsi/ginkgo/v2 v2.7.0/go.mod h1:yjiuMwPokqY1XauOgju45q3sJt6VzQ/Fict1LFVcsAo= 51 | github.com/onsi/ginkgo/v2 v2.8.1/go.mod h1:N1/NbDngAFcSLdyZ+/aYTYGSlq9qMCS/cNKGJjy+csc= 52 | github.com/onsi/ginkgo/v2 v2.9.0/go.mod h1:4xkjoL/tZv4SMWeww56BU5kAt19mVB47gTWxmrTcxyk= 53 | github.com/onsi/ginkgo/v2 v2.9.1/go.mod h1:FEcmzVcCHl+4o9bQZVab+4dC9+j+91t2FHSzmGAPfuo= 54 | github.com/onsi/ginkgo/v2 v2.9.2 h1:BA2GMJOtfGAfagzYtrAlufIP0lq6QERkFmHLMLPwFSU= 55 | github.com/onsi/ginkgo/v2 v2.9.2/go.mod h1:WHcJJG2dIlcCqVfBAwUCrJxSPFb6v4azBwgxeMeDuts= 56 | github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= 57 | github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= 58 | github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= 59 | github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= 60 | github.com/onsi/gomega v1.20.1/go.mod h1:DtrZpjmvpn2mPm4YWQa0/ALMDj9v4YxLgojwPeREyVo= 61 | github.com/onsi/gomega v1.21.1/go.mod h1:iYAIXgPSaDHak0LCMA+AWBpIKBr8WZicMxnE8luStNc= 62 | github.com/onsi/gomega v1.22.1/go.mod h1:x6n7VNe4hw0vkyYUM4mjIXx3JbLiPaBPNgB7PRQ1tuM= 63 | github.com/onsi/gomega v1.24.0/go.mod h1:Z/NWtiqwBrwUt4/2loMmHL63EDLnYHmVbuBpDr2vQAg= 64 | github.com/onsi/gomega v1.24.1/go.mod h1:3AOiACssS3/MajrniINInwbfOOtfZvplPzuRSmvt1jM= 65 | github.com/onsi/gomega v1.26.0/go.mod h1:r+zV744Re+DiYCIPRlYOTxn0YkOLcAnW8k1xXdMPGhM= 66 | github.com/onsi/gomega v1.27.1/go.mod h1:aHX5xOykVYzWOV4WqQy0sy8BQptgukenXpCXfadcIAw= 67 | github.com/onsi/gomega v1.27.3/go.mod h1:5vG284IBtfDAmDyrK+eGyZmUgUlmi+Wngqo557cZ6Gw= 68 | github.com/onsi/gomega v1.27.4/go.mod h1:riYq/GJKh8hhoM01HN6Vmuy93AarCXCBGpvFDK3q3fQ= 69 | github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE= 70 | github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg= 71 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 72 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 73 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 74 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 75 | github.com/seaguest/deepcopy v1.0.0 h1:wWQx83kOWN/vA5vMASw3IOBVlntiwz5XfTY5vSAeNhM= 76 | github.com/seaguest/deepcopy v1.0.0/go.mod h1:RQ8nkINn8oEO1mIwwh7IheYeKxybejxbA098apVijDc= 77 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 78 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 79 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 80 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 81 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 82 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 83 | github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 84 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 85 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 86 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 87 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 88 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 89 | golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= 90 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 91 | golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= 92 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 93 | golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI= 94 | golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 95 | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 96 | golang.org/x/mod v0.9.0 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs= 97 | golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 98 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 99 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 100 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 101 | golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 102 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 103 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 104 | golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= 105 | golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 106 | golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= 107 | golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= 108 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 109 | golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= 110 | golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= 111 | golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= 112 | golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= 113 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 114 | golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 115 | golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ= 116 | golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= 117 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 118 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 119 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 120 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 121 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 122 | golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= 123 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 124 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 125 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 126 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 127 | golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 128 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 129 | golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 130 | golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 131 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 132 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 133 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 134 | golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 135 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 136 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 137 | golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 138 | golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 139 | golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 140 | golang.org/x/sys v0.0.0-20220422013727-9388b58f7150/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 141 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 142 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 143 | golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 144 | golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 145 | golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 146 | golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 147 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 148 | golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= 149 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 150 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 151 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 152 | golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 153 | golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= 154 | golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= 155 | golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ= 156 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 157 | golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= 158 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 159 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 160 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 161 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 162 | golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 163 | golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 164 | golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 165 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 166 | golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68= 167 | golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 168 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 169 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 170 | golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 171 | golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= 172 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 173 | golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA= 174 | golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ= 175 | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 176 | golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4= 177 | golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= 178 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 179 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 180 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 181 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 182 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 183 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 184 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 185 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 186 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 187 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 188 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 189 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 190 | google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= 191 | google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 192 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 193 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 194 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 195 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 196 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 197 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 198 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 199 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 200 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 201 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 202 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 203 | -------------------------------------------------------------------------------- /item.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "encoding/json" 5 | "time" 6 | 7 | "google.golang.org/protobuf/encoding/protojson" 8 | "google.golang.org/protobuf/proto" 9 | ) 10 | 11 | type Item struct { 12 | Object interface{} `json:"object"` // object 13 | Size int `json:"size"` // object size, in bytes. 14 | ExpireAt int64 `json:"expire_at"` // data expiration timestamp. in milliseconds. 15 | } 16 | 17 | func newItem(v interface{}, ttl time.Duration) *Item { 18 | var expiredAt int64 19 | if ttl > 0 { 20 | expiredAt = time.Now().Add(ttl).UnixMilli() 21 | } 22 | 23 | return &Item{ 24 | Object: v, 25 | ExpireAt: expiredAt, 26 | } 27 | } 28 | 29 | func (it *Item) Expired() bool { 30 | return it.ExpireAt != 0 && it.ExpireAt < time.Now().UnixMilli() 31 | } 32 | 33 | func (i *Item) MarshalJSON() ([]byte, error) { 34 | type Alias Item 35 | obj := struct { 36 | Object json.RawMessage `json:"object"` 37 | *Alias 38 | }{ 39 | Alias: (*Alias)(i), 40 | } 41 | 42 | var err error 43 | if pm, ok := i.Object.(proto.Message); ok { 44 | // Use protojson for proto.Message 45 | obj.Object, err = protojson.Marshal(pm) 46 | } else { 47 | // Use json.Marshal for other types 48 | obj.Object, err = json.Marshal(i.Object) 49 | } 50 | if err != nil { 51 | return nil, err 52 | } 53 | return json.Marshal(obj) 54 | } 55 | 56 | func (i *Item) UnmarshalJSON(data []byte) error { 57 | type Alias Item 58 | aux := &struct { 59 | Object json.RawMessage `json:"object"` 60 | *Alias 61 | }{ 62 | Alias: (*Alias)(i), 63 | } 64 | 65 | if err := json.Unmarshal(data, &aux); err != nil { 66 | return err 67 | } 68 | 69 | // Replace this with your actual proto.Message 70 | if pm, ok := i.Object.(proto.Message); ok { 71 | if err := protojson.Unmarshal(aux.Object, pm); err != nil { 72 | return err 73 | } 74 | i.Object = pm 75 | return nil 76 | } 77 | return json.Unmarshal(aux.Object, i.Object) 78 | } 79 | -------------------------------------------------------------------------------- /mem_cache.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "strings" 5 | "sync" 6 | "time" 7 | ) 8 | 9 | type memCache struct { 10 | // local cache 11 | items sync.Map 12 | 13 | // clean interval 14 | ci time.Duration 15 | 16 | // metric for mem cache 17 | metric Metrics 18 | } 19 | 20 | // newMemCache memcache will scan all objects for every clean interval and delete expired key. 21 | func newMemCache(ci time.Duration, metric Metrics) *memCache { 22 | c := &memCache{ 23 | items: sync.Map{}, 24 | ci: ci, 25 | metric: metric, 26 | } 27 | 28 | go c.runJanitor() 29 | return c 30 | } 31 | 32 | // get an item from the memcache. Returns the item or nil, and a bool indicating whether the key was found. 33 | func (c *memCache) get(key string) *Item { 34 | var metricType string 35 | defer c.metric.Observe()(key, &metricType, nil) 36 | 37 | tmp, ok := c.items.Load(key) 38 | if !ok { 39 | metricType = MetricTypeGetMemMiss 40 | return nil 41 | } 42 | 43 | it := tmp.(*Item) 44 | if !it.Expired() { 45 | metricType = MetricTypeGetMemHit 46 | } else { 47 | metricType = MetricTypeGetMemExpired 48 | } 49 | return it 50 | } 51 | 52 | func (c *memCache) set(key string, it *Item) { 53 | // mem set 54 | defer c.metric.Observe()(key, MetricTypeSetMem, nil) 55 | 56 | c.items.Store(key, it) 57 | } 58 | 59 | // Delete an item from the memcache. Does nothing if the key is not in the memcache. 60 | func (c *memCache) delete(key string) { 61 | // mem del 62 | defer c.metric.Observe()(key, MetricTypeDeleteMem, nil) 63 | 64 | c.items.Delete(key) 65 | } 66 | 67 | // start key scanning to delete expired keys 68 | func (c *memCache) runJanitor() { 69 | ticker := time.NewTicker(c.ci) 70 | defer ticker.Stop() 71 | 72 | for { 73 | select { 74 | case <-ticker.C: 75 | c.DeleteExpired() 76 | } 77 | } 78 | } 79 | 80 | type memStat struct { 81 | count int 82 | memUsage int 83 | } 84 | 85 | // DeleteExpired delete all expired items from the memcache. 86 | func (c *memCache) DeleteExpired() { 87 | ms := make(map[string]*memStat) 88 | c.items.Range(func(key, value interface{}) bool { 89 | v := value.(*Item) 90 | k := key.(string) 91 | 92 | objectType := strings.Split(strings.TrimPrefix(k, c.metric.namespace+":"), c.metric.separator)[0] 93 | stat, ok := ms[objectType] 94 | if !ok { 95 | stat = &memStat{ 96 | count: 1, 97 | memUsage: v.Size, 98 | } 99 | } else { 100 | stat.count += 1 101 | stat.memUsage += v.Size 102 | } 103 | ms[objectType] = stat 104 | 105 | // delete outdated for memory cache 106 | if v.Expired() { 107 | c.items.Delete(k) 108 | } 109 | return true 110 | }) 111 | 112 | for k, v := range ms { 113 | c.metric.Set(k, MetricTypeCount, v.count) 114 | c.metric.Set(k, MetricTypeMemUsage, v.memUsage) 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /metrics.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "strings" 5 | "time" 6 | ) 7 | 8 | const ( 9 | MetricTypeGetMemHit = "get_mem_hit" 10 | MetricTypeGetMemMiss = "get_mem_miss" 11 | MetricTypeGetMemExpired = "get_mem_expired" 12 | MetricTypeGetRedisHit = "get_redis_hit" 13 | MetricTypeGetRedisMiss = "get_redis_miss" 14 | MetricTypeGetRedisExpired = "get_redis_expired" 15 | MetricTypeGetCache = "get_cache" 16 | MetricTypeLoad = "load" 17 | MetricTypeAsyncLoad = "async_load" 18 | MetricTypeSetCache = "set_cache" 19 | MetricTypeSetMem = "set_mem" 20 | MetricTypeSetRedis = "set_redis" 21 | MetricTypeDeleteCache = "del_cache" 22 | MetricTypeDeleteMem = "del_mem" 23 | MetricTypeDeleteRedis = "del_redis" 24 | MetricTypeCount = "count" 25 | MetricTypeMemUsage = "mem_usage" 26 | ) 27 | 28 | type Metrics struct { 29 | // keys are namespacedKey, need trim namespace 30 | namespace string 31 | 32 | separator string 33 | 34 | onMetric func(key, objectType string, metricType string, count int, elapsedTime time.Duration) 35 | } 36 | 37 | // Observe used for histogram metrics 38 | func (m Metrics) Observe() func(string, interface{}, *error) { 39 | start := time.Now() 40 | return func(namespacedKey string, metricType interface{}, err *error) { 41 | if m.onMetric == nil { 42 | return 43 | } 44 | // ignore metric for error case 45 | if err != nil && *err != nil { 46 | return 47 | } 48 | 49 | var metric string 50 | switch v := metricType.(type) { 51 | case *string: 52 | metric = *v 53 | case string: 54 | metric = v 55 | default: 56 | return 57 | } 58 | key := strings.TrimPrefix(namespacedKey, m.namespace+":") 59 | objectType := strings.Split(key, m.separator)[0] 60 | m.onMetric(key, objectType, metric, 0, time.Since(start)) 61 | } 62 | } 63 | 64 | // Set used for gauge metrics, counts and memory usage metrics 65 | func (m Metrics) Set(objectType, metric string, count int) { 66 | m.onMetric("*", objectType, metric, count, 0) 67 | } 68 | -------------------------------------------------------------------------------- /option.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/gomodule/redigo/redis" 8 | ) 9 | 10 | type GetCachePolicy int 11 | 12 | const ( 13 | GetPolicyReturnExpired GetCachePolicy = iota + 1 14 | GetPolicyReloadOnExpiry 15 | ) 16 | 17 | type UpdateCachePolicy int 18 | 19 | const ( 20 | UpdatePolicyBroadcast UpdateCachePolicy = iota + 1 21 | UpdatePolicyNoBroadcast 22 | ) 23 | 24 | type Options struct { 25 | Namespace string 26 | 27 | // key should be in format object_type{Separator}id 28 | // can be : or ; or # 29 | Separator string 30 | 31 | // clean interval for in-memory cache 32 | CleanInterval time.Duration 33 | 34 | // get policy when data is expired, ReturnExpired or ReloadOnExpiry 35 | GetPolicy GetCachePolicy 36 | 37 | // update policy when data is updated, Broadcast or NoBroadcast 38 | UpdatePolicy UpdateCachePolicy 39 | 40 | // will call loader function when disabled id true 41 | Disabled bool 42 | 43 | // redis ttl = ttl*RedisTTLFactor, data in redis lives longer than memory cache. 44 | RedisTTLFactor int 45 | 46 | // retrieve redis connection 47 | GetConn func() redis.Conn 48 | 49 | // metrics 50 | Metric Metrics 51 | 52 | // must be provided for cache initialization, handle internal error 53 | OnError func(ctx context.Context, err error) 54 | } 55 | 56 | type Option func(*Options) 57 | 58 | func Namespace(namespace string) Option { 59 | return func(o *Options) { 60 | o.Namespace = namespace 61 | } 62 | } 63 | 64 | func Separator(separator string) Option { 65 | return func(o *Options) { 66 | o.Separator = separator 67 | } 68 | } 69 | 70 | func CleanInterval(cleanInterval time.Duration) Option { 71 | return func(o *Options) { 72 | o.CleanInterval = cleanInterval 73 | } 74 | } 75 | 76 | func Disabled(disabled bool) Option { 77 | return func(o *Options) { 78 | o.Disabled = disabled 79 | } 80 | } 81 | 82 | func RedisTTLFactor(redisTTLFactor int) Option { 83 | return func(o *Options) { 84 | o.RedisTTLFactor = redisTTLFactor 85 | } 86 | } 87 | 88 | func GetConn(getConn func() redis.Conn) Option { 89 | return func(o *Options) { 90 | o.GetConn = getConn 91 | } 92 | } 93 | 94 | func OnMetric(onMetric func(key, objectType string, metricType string, count int, elapsedTime time.Duration)) Option { 95 | return func(o *Options) { 96 | o.Metric = Metrics{ 97 | onMetric: onMetric, 98 | } 99 | } 100 | } 101 | 102 | func OnError(onError func(ctx context.Context, err error)) Option { 103 | return func(o *Options) { 104 | o.OnError = onError 105 | } 106 | } 107 | 108 | func GetPolicy(getPolicy GetCachePolicy) Option { 109 | return func(o *Options) { 110 | o.GetPolicy = getPolicy 111 | } 112 | } 113 | 114 | func UpdatePolicy(updatePolicy UpdateCachePolicy) Option { 115 | return func(o *Options) { 116 | o.UpdatePolicy = updatePolicy 117 | } 118 | } 119 | 120 | func newOptions(opts ...Option) Options { 121 | opt := Options{} 122 | for _, o := range opts { 123 | o(&opt) 124 | } 125 | return opt 126 | } 127 | -------------------------------------------------------------------------------- /redis_cache.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/gomodule/redigo/redis" 7 | ) 8 | 9 | type redisCache struct { 10 | // func to get redis conn from pool 11 | getConn func() redis.Conn 12 | 13 | // TTL in redis will be redisTTLFactor*mem_ttl 14 | redisTTLFactor int 15 | 16 | // metric for redis cache 17 | metric Metrics 18 | } 19 | 20 | func newRedisCache(getConn func() redis.Conn, redisTTLFactor int, metric Metrics) *redisCache { 21 | return &redisCache{ 22 | getConn: getConn, 23 | redisTTLFactor: redisTTLFactor, 24 | metric: metric, 25 | } 26 | } 27 | 28 | // read item from redis 29 | func (c *redisCache) get(key string, obj interface{}) (it *Item, err error) { 30 | var metricType string 31 | defer c.metric.Observe()(key, &metricType, &err) 32 | 33 | body, err := c.getString(key) 34 | if err != nil { 35 | if err == redis.ErrNil { 36 | metricType = MetricTypeGetRedisMiss 37 | err = nil 38 | return 39 | } else { 40 | return 41 | } 42 | } 43 | 44 | it = &Item{} 45 | it.Object = obj 46 | err = unmarshal([]byte(body), it) 47 | if err != nil { 48 | return 49 | } 50 | 51 | if !it.Expired() { 52 | metricType = MetricTypeGetRedisHit 53 | } else { 54 | metricType = MetricTypeGetRedisExpired 55 | } 56 | it.Size = len(body) 57 | return 58 | } 59 | 60 | func (c *redisCache) set(key string, obj interface{}, ttl time.Duration) (it *Item, err error) { 61 | // redis set 62 | defer c.metric.Observe()(key, MetricTypeSetRedis, &err) 63 | 64 | it = newItem(obj, ttl) 65 | redisTTL := 0 66 | if ttl > 0 { 67 | redisTTL = int(ttl/time.Second) * c.redisTTLFactor 68 | } 69 | 70 | bs, err := marshal(it) 71 | if err != nil { 72 | return 73 | } 74 | 75 | err = c.setString(key, string(bs), redisTTL) 76 | if err != nil { 77 | return 78 | } 79 | return 80 | } 81 | 82 | func (c *redisCache) delete(key string) (err error) { 83 | // redis del 84 | defer c.metric.Observe()(key, MetricTypeDeleteRedis, &err) 85 | 86 | conn := c.getConn() 87 | defer conn.Close() 88 | 89 | _, err = conn.Do("DEL", key) 90 | return 91 | } 92 | 93 | func (c *redisCache) setString(key, value string, ttl int) (err error) { 94 | conn := c.getConn() 95 | defer conn.Close() 96 | 97 | if ttl == 0 { 98 | _, err = conn.Do("SET", key, value) 99 | } else { 100 | _, err = conn.Do("SETEX", key, ttl, value) 101 | } 102 | return 103 | } 104 | 105 | func (c *redisCache) getString(key string) (value string, err error) { 106 | conn := c.getConn() 107 | defer conn.Close() 108 | 109 | value, err = redis.String(conn.Do("GET", key)) 110 | return 111 | } 112 | --------------------------------------------------------------------------------