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 }