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  }