github.com/thanos-io/thanos@v0.32.5/internal/cortex/chunk/cache/fifo_cache.go (about)

     1  // Copyright (c) The Cortex Authors.
     2  // Licensed under the Apache License 2.0.
     3  
     4  package cache
     5  
     6  import (
     7  	"container/list"
     8  	"context"
     9  	"flag"
    10  	"sync"
    11  	"time"
    12  	"unsafe"
    13  
    14  	"github.com/dustin/go-humanize"
    15  	"github.com/go-kit/log"
    16  	"github.com/go-kit/log/level"
    17  	"github.com/pkg/errors"
    18  	"github.com/prometheus/client_golang/prometheus"
    19  	"github.com/prometheus/client_golang/prometheus/promauto"
    20  )
    21  
    22  const (
    23  	elementSize    = int(unsafe.Sizeof(list.Element{}))
    24  	elementPrtSize = int(unsafe.Sizeof(&list.Element{}))
    25  )
    26  
    27  // This FIFO cache implementation supports two eviction methods - based on number of items in the cache, and based on memory usage.
    28  // For the memory-based eviction, set FifoCacheConfig.MaxSizeBytes to a positive integer, indicating upper limit of memory allocated by items in the cache.
    29  // Alternatively, set FifoCacheConfig.MaxSizeItems to a positive integer, indicating maximum number of items in the cache.
    30  // If both parameters are set, both methods are enforced, whichever hits first.
    31  
    32  // FifoCacheConfig holds config for the FifoCache.
    33  type FifoCacheConfig struct {
    34  	MaxSizeBytes string        `yaml:"max_size_bytes"`
    35  	MaxSizeItems int           `yaml:"max_size_items"`
    36  	Validity     time.Duration `yaml:"validity"`
    37  
    38  	DeprecatedSize int `yaml:"size"`
    39  }
    40  
    41  // RegisterFlagsWithPrefix adds the flags required to config this to the given FlagSet
    42  func (cfg *FifoCacheConfig) RegisterFlagsWithPrefix(prefix, description string, f *flag.FlagSet) {
    43  	f.StringVar(&cfg.MaxSizeBytes, prefix+"fifocache.max-size-bytes", "", description+"Maximum memory size of the cache in bytes. A unit suffix (KB, MB, GB) may be applied.")
    44  	f.IntVar(&cfg.MaxSizeItems, prefix+"fifocache.max-size-items", 0, description+"Maximum number of entries in the cache.")
    45  	f.DurationVar(&cfg.Validity, prefix+"fifocache.duration", 0, description+"The expiry duration for the cache.")
    46  
    47  	f.IntVar(&cfg.DeprecatedSize, prefix+"fifocache.size", 0, "Deprecated (use max-size-items or max-size-bytes instead): "+description+"The number of entries to cache. ")
    48  }
    49  
    50  func (cfg *FifoCacheConfig) Validate() error {
    51  	_, err := parsebytes(cfg.MaxSizeBytes)
    52  	return err
    53  }
    54  
    55  func parsebytes(s string) (uint64, error) {
    56  	if len(s) == 0 {
    57  		return 0, nil
    58  	}
    59  	bytes, err := humanize.ParseBytes(s)
    60  	if err != nil {
    61  		return 0, errors.Wrap(err, "invalid FifoCache config")
    62  	}
    63  	return bytes, nil
    64  }
    65  
    66  // FifoCache is a simple string -> interface{} cache which uses a fifo slide to
    67  // manage evictions.  O(1) inserts and updates, O(1) gets.
    68  type FifoCache struct {
    69  	lock          sync.RWMutex
    70  	maxSizeItems  int
    71  	maxSizeBytes  uint64
    72  	currSizeBytes uint64
    73  	validity      time.Duration
    74  
    75  	entries map[string]*list.Element
    76  	lru     *list.List
    77  
    78  	entriesAdded    prometheus.Counter
    79  	entriesAddedNew prometheus.Counter
    80  	entriesEvicted  prometheus.Counter
    81  	entriesCurrent  prometheus.Gauge
    82  	totalGets       prometheus.Counter
    83  	totalMisses     prometheus.Counter
    84  	staleGets       prometheus.Counter
    85  	memoryBytes     prometheus.Gauge
    86  }
    87  
    88  type cacheEntry struct {
    89  	updated time.Time
    90  	key     string
    91  	value   []byte
    92  }
    93  
    94  // NewFifoCache returns a new initialised FifoCache of size.
    95  func NewFifoCache(name string, cfg FifoCacheConfig, reg prometheus.Registerer, logger log.Logger) *FifoCache {
    96  	if cfg.DeprecatedSize > 0 {
    97  		level.Warn(logger).Log("msg", "running with DEPRECATED flag fifocache.size, use fifocache.max-size-items or fifocache.max-size-bytes instead", "cache", name)
    98  		cfg.MaxSizeItems = cfg.DeprecatedSize
    99  	}
   100  	maxSizeBytes, _ := parsebytes(cfg.MaxSizeBytes)
   101  
   102  	if maxSizeBytes == 0 && cfg.MaxSizeItems == 0 {
   103  		// zero cache capacity - no need to create cache
   104  		level.Warn(logger).Log("msg", "neither fifocache.max-size-bytes nor fifocache.max-size-items is set", "cache", name)
   105  		return nil
   106  	}
   107  	return &FifoCache{
   108  		maxSizeItems: cfg.MaxSizeItems,
   109  		maxSizeBytes: maxSizeBytes,
   110  		validity:     cfg.Validity,
   111  		entries:      make(map[string]*list.Element),
   112  		lru:          list.New(),
   113  
   114  		entriesAdded: promauto.With(reg).NewCounter(prometheus.CounterOpts{
   115  			Namespace:   "querier",
   116  			Subsystem:   "cache",
   117  			Name:        "added_total",
   118  			Help:        "The total number of Put calls on the cache",
   119  			ConstLabels: prometheus.Labels{"cache": name},
   120  		}),
   121  
   122  		entriesAddedNew: promauto.With(reg).NewCounter(prometheus.CounterOpts{
   123  			Namespace:   "querier",
   124  			Subsystem:   "cache",
   125  			Name:        "added_new_total",
   126  			Help:        "The total number of new entries added to the cache",
   127  			ConstLabels: prometheus.Labels{"cache": name},
   128  		}),
   129  
   130  		entriesEvicted: promauto.With(reg).NewCounter(prometheus.CounterOpts{
   131  			Namespace:   "querier",
   132  			Subsystem:   "cache",
   133  			Name:        "evicted_total",
   134  			Help:        "The total number of evicted entries",
   135  			ConstLabels: prometheus.Labels{"cache": name},
   136  		}),
   137  
   138  		entriesCurrent: promauto.With(reg).NewGauge(prometheus.GaugeOpts{
   139  			Namespace:   "querier",
   140  			Subsystem:   "cache",
   141  			Name:        "entries",
   142  			Help:        "The total number of entries",
   143  			ConstLabels: prometheus.Labels{"cache": name},
   144  		}),
   145  
   146  		totalGets: promauto.With(reg).NewCounter(prometheus.CounterOpts{
   147  			Namespace:   "querier",
   148  			Subsystem:   "cache",
   149  			Name:        "gets_total",
   150  			Help:        "The total number of Get calls",
   151  			ConstLabels: prometheus.Labels{"cache": name},
   152  		}),
   153  
   154  		totalMisses: promauto.With(reg).NewCounter(prometheus.CounterOpts{
   155  			Namespace:   "querier",
   156  			Subsystem:   "cache",
   157  			Name:        "misses_total",
   158  			Help:        "The total number of Get calls that had no valid entry",
   159  			ConstLabels: prometheus.Labels{"cache": name},
   160  		}),
   161  
   162  		staleGets: promauto.With(reg).NewCounter(prometheus.CounterOpts{
   163  			Namespace:   "querier",
   164  			Subsystem:   "cache",
   165  			Name:        "stale_gets_total",
   166  			Help:        "The total number of Get calls that had an entry which expired",
   167  			ConstLabels: prometheus.Labels{"cache": name},
   168  		}),
   169  
   170  		memoryBytes: promauto.With(reg).NewGauge(prometheus.GaugeOpts{
   171  			Namespace:   "querier",
   172  			Subsystem:   "cache",
   173  			Name:        "memory_bytes",
   174  			Help:        "The current cache size in bytes",
   175  			ConstLabels: prometheus.Labels{"cache": name},
   176  		}),
   177  	}
   178  }
   179  
   180  // Fetch implements Cache.
   181  func (c *FifoCache) Fetch(ctx context.Context, keys []string) (found []string, bufs [][]byte, missing []string) {
   182  	found, missing, bufs = make([]string, 0, len(keys)), make([]string, 0, len(keys)), make([][]byte, 0, len(keys))
   183  	for _, key := range keys {
   184  		val, ok := c.Get(ctx, key)
   185  		if !ok {
   186  			missing = append(missing, key)
   187  			continue
   188  		}
   189  
   190  		found = append(found, key)
   191  		bufs = append(bufs, val)
   192  	}
   193  	return
   194  }
   195  
   196  // Store implements Cache.
   197  func (c *FifoCache) Store(ctx context.Context, keys []string, values [][]byte) {
   198  	c.entriesAdded.Inc()
   199  
   200  	c.lock.Lock()
   201  	defer c.lock.Unlock()
   202  
   203  	for i := range keys {
   204  		c.put(keys[i], values[i])
   205  	}
   206  }
   207  
   208  // Stop implements Cache.
   209  func (c *FifoCache) Stop() {
   210  	c.lock.Lock()
   211  	defer c.lock.Unlock()
   212  
   213  	c.entriesEvicted.Add(float64(c.lru.Len()))
   214  
   215  	c.entries = make(map[string]*list.Element)
   216  	c.lru.Init()
   217  	c.currSizeBytes = 0
   218  
   219  	c.entriesCurrent.Set(float64(0))
   220  	c.memoryBytes.Set(float64(0))
   221  }
   222  
   223  func (c *FifoCache) put(key string, value []byte) {
   224  	// See if we already have the item in the cache.
   225  	element, ok := c.entries[key]
   226  	if ok {
   227  		// Remove the item from the cache.
   228  		entry := c.lru.Remove(element).(*cacheEntry)
   229  		delete(c.entries, key)
   230  		c.currSizeBytes -= sizeOf(entry)
   231  		c.entriesCurrent.Dec()
   232  	}
   233  
   234  	entry := &cacheEntry{
   235  		updated: time.Now(),
   236  		key:     key,
   237  		value:   value,
   238  	}
   239  	entrySz := sizeOf(entry)
   240  
   241  	if c.maxSizeBytes > 0 && entrySz > c.maxSizeBytes {
   242  		// Cannot keep this item in the cache.
   243  		if ok {
   244  			// We do not replace this item.
   245  			c.entriesEvicted.Inc()
   246  		}
   247  		c.memoryBytes.Set(float64(c.currSizeBytes))
   248  		return
   249  	}
   250  
   251  	// Otherwise, see if we need to evict item(s).
   252  	for (c.maxSizeBytes > 0 && c.currSizeBytes+entrySz > c.maxSizeBytes) || (c.maxSizeItems > 0 && len(c.entries) >= c.maxSizeItems) {
   253  		lastElement := c.lru.Back()
   254  		if lastElement == nil {
   255  			break
   256  		}
   257  		evicted := c.lru.Remove(lastElement).(*cacheEntry)
   258  		delete(c.entries, evicted.key)
   259  		c.currSizeBytes -= sizeOf(evicted)
   260  		c.entriesCurrent.Dec()
   261  		c.entriesEvicted.Inc()
   262  	}
   263  
   264  	// Finally, we have space to add the item.
   265  	c.entries[key] = c.lru.PushFront(entry)
   266  	c.currSizeBytes += entrySz
   267  	if !ok {
   268  		c.entriesAddedNew.Inc()
   269  	}
   270  	c.entriesCurrent.Inc()
   271  	c.memoryBytes.Set(float64(c.currSizeBytes))
   272  }
   273  
   274  // Get returns the stored value against the key and when the key was last updated.
   275  func (c *FifoCache) Get(ctx context.Context, key string) ([]byte, bool) {
   276  	c.totalGets.Inc()
   277  
   278  	c.lock.RLock()
   279  	defer c.lock.RUnlock()
   280  
   281  	element, ok := c.entries[key]
   282  	if ok {
   283  		entry := element.Value.(*cacheEntry)
   284  		if c.validity == 0 || time.Since(entry.updated) < c.validity {
   285  			return entry.value, true
   286  		}
   287  
   288  		c.totalMisses.Inc()
   289  		c.staleGets.Inc()
   290  		return nil, false
   291  	}
   292  
   293  	c.totalMisses.Inc()
   294  	return nil, false
   295  }
   296  
   297  func sizeOf(item *cacheEntry) uint64 {
   298  	return uint64(int(unsafe.Sizeof(*item)) + // size of cacheEntry
   299  		len(item.key) + // size of key
   300  		cap(item.value) + // size of value
   301  		elementSize + // size of the element in linked list
   302  		elementPrtSize) // size of the pointer to an element in the map
   303  }