vitess.io/vitess@v0.16.2/go/vt/vtadmin/cache/cache.go (about) 1 /* 2 Copyright 2022 The Vitess Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 // Package cache provides a generic key/value cache with support for background 18 // filling. 19 package cache 20 21 import ( 22 "context" 23 "sync" 24 "time" 25 26 "github.com/patrickmn/go-cache" 27 28 "vitess.io/vitess/go/vt/log" 29 ) 30 31 // Keyer is the interface cache keys implement to turn themselves into string 32 // keys. 33 // 34 // Note: we define this type rather than using Stringer so users may implement 35 // that interface for different string representation needs, for example 36 // providing a human-friendly representation. 37 type Keyer interface{ Key() string } 38 39 const ( 40 // DefaultExpiration is used to instruct calls to Add to use the default 41 // cache expiration. 42 // See https://pkg.go.dev/github.com/patrickmn/go-cache@v2.1.0+incompatible#pkg-constants. 43 DefaultExpiration = cache.DefaultExpiration 44 // NoExpiration is used to create caches that do not expire items by 45 // default. 46 // See https://pkg.go.dev/github.com/patrickmn/go-cache@v2.1.0+incompatible#pkg-constants. 47 NoExpiration = cache.NoExpiration 48 49 // DefaultBackfillEnqueueWaitTime is the default value used for waiting to 50 // enqueue backfill requests, if a config is passed with a non-positive 51 // BackfillEnqueueWaitTime. 52 DefaultBackfillEnqueueWaitTime = time.Millisecond * 50 53 // DefaultBackfillRequestTTL is the default value used for how stale of 54 // backfill requests to still process, if a config is passed with a 55 // non-positive BackfillRequestTTL. 56 DefaultBackfillRequestTTL = time.Millisecond * 100 57 ) 58 59 // Config is the configuration for a cache. 60 type Config struct { 61 // DefaultExpiration is how long to keep Values in the cache by default (the 62 // duration passed to Add takes precedence). Use the sentinel NoExpiration 63 // to make Values never expire by default. 64 DefaultExpiration time.Duration `json:"default_expiration"` 65 // CleanupInterval is how often to remove expired Values from the cache. 66 CleanupInterval time.Duration `json:"cleanup_interval"` 67 68 // BackfillRequestTTL is how long a backfill request is considered valid. 69 // If the backfill goroutine encounters a request older than this, it is 70 // discarded. 71 BackfillRequestTTL time.Duration `json:"backfill_request_ttl"` 72 // BackfillRequestDuplicateInterval is how much time must pass before the 73 // backfill goroutine will re-backfill the same key. It is used to prevent 74 // multiple callers queuing up too many requests for the same key, when one 75 // backfill would satisfy all of them. 76 BackfillRequestDuplicateInterval time.Duration `json:"backfill_request_duplicate_interval"` 77 // BackfillQueueSize is how many outstanding backfill requests to permit. 78 // If the queue is full, calls to EnqueueBackfill will return false and 79 // those requests will be discarded. 80 BackfillQueueSize int `json:"backfill_queue_size"` 81 // BackfillEnqueueWaitTime is how long to wait when attempting to enqueue a 82 // backfill request before giving up. 83 BackfillEnqueueWaitTime time.Duration `json:"backfill_enqueue_wait_time"` 84 } 85 86 // Cache is a generic cache supporting background fills. To add things to the 87 // cache, call Add. To enqueue a background fill, call EnqueueBackfill with a 88 // Keyer implementation, which will be passed to the fill func provided to New. 89 // 90 // For example, to create a schema cache that can backfill full payloads (including 91 // size aggregation): 92 // 93 // var c *cache.Cache[BackfillSchemaRequest, *vtadminpb.Schema] 94 // c := cache.New(func(ctx context.Context, req BackfillSchemaRequest) (*vtadminpb.Schema, error) { 95 // // Fetch schema based on fields in `req`. 96 // // If err is nil, the backfilled schema will be added to the cache. 97 // return cluster.fetchSchema(ctx, req) 98 // }) 99 type Cache[Key Keyer, Value any] struct { 100 cache *cache.Cache 101 102 m sync.Mutex 103 lastFill map[string]time.Time 104 105 ctx context.Context 106 cancel context.CancelFunc 107 wg sync.WaitGroup 108 backfills chan *backfillRequest[Key] 109 110 fillFunc func(ctx context.Context, k Key) (Value, error) 111 112 cfg Config 113 } 114 115 // New creates a new cache with the given backfill func. When a request is 116 // enqueued (via EnqueueBackfill), fillFunc will be called with that request. 117 func New[Key Keyer, Value any](fillFunc func(ctx context.Context, req Key) (Value, error), cfg Config) *Cache[Key, Value] { 118 if cfg.BackfillEnqueueWaitTime <= 0 { 119 log.Warningf("BackfillEnqueueWaitTime (%v) must be positive, defaulting to %v", cfg.BackfillEnqueueWaitTime, DefaultBackfillEnqueueWaitTime) 120 cfg.BackfillEnqueueWaitTime = DefaultBackfillEnqueueWaitTime 121 } 122 123 if cfg.BackfillRequestTTL <= 0 { 124 log.Warningf("BackfillRequestTTL (%v) must be positive, defaulting to %v", cfg.BackfillRequestTTL, DefaultBackfillRequestTTL) 125 cfg.BackfillRequestTTL = DefaultBackfillRequestTTL 126 } 127 128 c := &Cache[Key, Value]{ 129 cache: cache.New(cfg.DefaultExpiration, cfg.CleanupInterval), 130 lastFill: map[string]time.Time{}, 131 backfills: make(chan *backfillRequest[Key], cfg.BackfillQueueSize), 132 fillFunc: fillFunc, 133 cfg: cfg, 134 } 135 136 c.ctx, c.cancel = context.WithCancel(context.Background()) 137 138 c.wg.Add(1) 139 go c.backfill() // TODO: consider allowing N backfill threads to run, configurable 140 141 return c 142 } 143 144 // Add adds a (key, value) to the cache directly, following the semantics of 145 // (github.com/patrickmn/go-cache).Cache.Add. 146 func (c *Cache[Key, Value]) Add(key Key, val Value, d time.Duration) error { 147 return c.add(key.Key(), val, d) 148 } 149 150 func (c *Cache[Key, Value]) add(key string, val Value, d time.Duration) error { 151 c.m.Lock() 152 // Record the time we last cached this key, to check against 153 c.lastFill[key] = time.Now().UTC() 154 c.m.Unlock() 155 156 // Then cache the actual value. 157 return c.cache.Add(key, val, d) 158 } 159 160 // Get returns the Value stored for the key, if present in the cache. If the key 161 // is not cached, the zero value for the given type is returned, along with a 162 // boolean to indicated presence/absence. 163 func (c *Cache[Key, Value]) Get(key Key) (Value, bool) { 164 v, exp, ok := c.cache.GetWithExpiration(key.Key()) 165 if !ok || (!exp.IsZero() && exp.Before(time.Now())) { 166 var zero Value 167 return zero, false 168 } 169 170 return v.(Value), ok 171 } 172 173 // EnqueueBackfill submits a request to the backfill queue. 174 func (c *Cache[Key, Value]) EnqueueBackfill(k Key) bool { 175 req := &backfillRequest[Key]{ 176 k: k, 177 requestedAt: time.Now().UTC(), 178 } 179 180 select { 181 case c.backfills <- req: 182 return true 183 case <-time.After(c.cfg.BackfillEnqueueWaitTime): 184 return false 185 } 186 } 187 188 // Close closes the backfill goroutine, effectively rendering this cache 189 // unusable for further background fills. 190 func (c *Cache[Key, Value]) Close() error { 191 c.cancel() 192 c.wg.Wait() 193 return nil 194 } 195 196 type backfillRequest[Key Keyer] struct { 197 k Key 198 requestedAt time.Time 199 } 200 201 func (c *Cache[Key, Value]) backfill() { 202 defer c.wg.Done() 203 204 for { 205 var req *backfillRequest[Key] 206 select { 207 case <-c.ctx.Done(): 208 return 209 case req = <-c.backfills: 210 } 211 212 if req.requestedAt.Add(c.cfg.BackfillRequestTTL).Before(time.Now()) { 213 // We took too long to get to this request, per config options. 214 log.Warningf("backfill for %s requested at %s; discarding due to exceeding TTL (%s)", req.k.Key(), req.requestedAt, c.cfg.BackfillRequestTTL) 215 continue 216 } 217 218 key := req.k.Key() 219 220 c.m.Lock() 221 if t, ok := c.lastFill[key]; ok { 222 if !t.IsZero() && t.Add(c.cfg.BackfillRequestDuplicateInterval).After(time.Now()) { 223 // We recently added a value for this key to the cache, either via 224 // another backfill request, or directly via a call to Add. 225 log.Infof("filled cache for %s less than %s ago (at %s)", key, c.cfg.BackfillRequestDuplicateInterval, t.UTC()) 226 c.m.Unlock() 227 continue 228 } 229 230 // NOTE: In the strictest sense, we would `delete(lastFill, key)` 231 // here. However, we're about to fill that key again, so we'd be 232 // immediately re-adding the key we just deleted from `lastFill`. 233 // 234 // We do *not* apply the same treatment to the actual Value cache, 235 // because go-cache is running a background thread to periodically 236 // clean up expired entries. 237 } 238 c.m.Unlock() 239 240 val, err := c.fillFunc(c.ctx, req.k) 241 if err != nil { 242 log.Errorf("backfill failed for key %s: %s", key, err) 243 // TODO: consider re-requesting with a retry-counter paired with a config to give up after N attempts 244 continue 245 } 246 247 // Finally, store the value. 248 if err := c.add(key, val, cache.DefaultExpiration); err != nil { 249 log.Warningf("failed to add (%s, %+v) to cache: %s", key, val, err) 250 } 251 } 252 } 253 254 // Debug implements debug.Debuggable for Cache. 255 func (c *Cache[Key, Value]) Debug() map[string]any { 256 return map[string]any{ 257 "size": c.cache.ItemCount(), // NOTE: this may include expired items that have not been evicted yet. 258 "config": c.cfg, 259 "backfill_queue_length": len(c.backfills), 260 "closed": c.ctx.Err() != nil, 261 } 262 }