• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

dgraph-io / ristretto / 6040586084

31 Aug 2023 06:52AM UTC coverage: 73.641% (-0.05%) from 73.689%
6040586084

push

web-flow
set missing Expiration field on evicted items (#345)

On our use case we need to rely on expiration timestamps but we realized
that they return zero timestamps. Our assumption is that this is due to
a bug and not by design.

## Problem
When `OnEvict` callback is executed `Item` does not have `Expiration`
field set. Considering it is a public field this needs to be set and
available.

## Solution
Initialize `Expiration` field before calling the `onEvict` callback

6 of 6 new or added lines in 1 file covered. (100.0%)

2221 of 3016 relevant lines covered (73.64%)

2507101.68 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

90.84
/cache.go
1
/*
2
 * Copyright 2019 Dgraph Labs, Inc. and Contributors
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
// Ristretto is a fast, fixed size, in-memory cache with a dual focus on
18
// throughput and hit ratio performance. You can easily add Ristretto to an
19
// existing system and keep the most valuable data where you need it.
20
package ristretto
21

22
import (
23
        "bytes"
24
        "errors"
25
        "fmt"
26
        "sync"
27
        "sync/atomic"
28
        "time"
29
        "unsafe"
30

31
        "github.com/dgraph-io/ristretto/z"
32
)
33

34
var (
35
        // TODO: find the optimal value for this or make it configurable
36
        setBufSize = 32 * 1024
37
)
38

39
type itemCallback func(*Item)
40

41
const itemSize = int64(unsafe.Sizeof(storeItem{}))
42

43
// Cache is a thread-safe implementation of a hashmap with a TinyLFU admission
44
// policy and a Sampled LFU eviction policy. You can use the same Cache instance
45
// from as many goroutines as you want.
46
type Cache struct {
47
        // store is the central concurrent hashmap where key-value items are stored.
48
        store store
49
        // policy determines what gets let in to the cache and what gets kicked out.
50
        policy policy
51
        // getBuf is a custom ring buffer implementation that gets pushed to when
52
        // keys are read.
53
        getBuf *ringBuffer
54
        // setBuf is a buffer allowing us to batch/drop Sets during times of high
55
        // contention.
56
        setBuf chan *Item
57
        // onEvict is called for item evictions.
58
        onEvict itemCallback
59
        // onReject is called when an item is rejected via admission policy.
60
        onReject itemCallback
61
        // onExit is called whenever a value goes out of scope from the cache.
62
        onExit (func(interface{}))
63
        // KeyToHash function is used to customize the key hashing algorithm.
64
        // Each key will be hashed using the provided function. If keyToHash value
65
        // is not set, the default keyToHash function is used.
66
        keyToHash func(interface{}) (uint64, uint64)
67
        // stop is used to stop the processItems goroutine.
68
        stop chan struct{}
69
        // indicates whether cache is closed.
70
        isClosed bool
71
        // cost calculates cost from a value.
72
        cost func(value interface{}) int64
73
        // ignoreInternalCost dictates whether to ignore the cost of internally storing
74
        // the item in the cost calculation.
75
        ignoreInternalCost bool
76
        // cleanupTicker is used to periodically check for entries whose TTL has passed.
77
        cleanupTicker *time.Ticker
78
        // Metrics contains a running log of important statistics like hits, misses,
79
        // and dropped items.
80
        Metrics *Metrics
81
}
82

83
// Config is passed to NewCache for creating new Cache instances.
84
type Config struct {
85
        // NumCounters determines the number of counters (keys) to keep that hold
86
        // access frequency information. It's generally a good idea to have more
87
        // counters than the max cache capacity, as this will improve eviction
88
        // accuracy and subsequent hit ratios.
89
        //
90
        // For example, if you expect your cache to hold 1,000,000 items when full,
91
        // NumCounters should be 10,000,000 (10x). Each counter takes up roughly
92
        // 3 bytes (4 bits for each counter * 4 copies plus about a byte per
93
        // counter for the bloom filter). Note that the number of counters is
94
        // internally rounded up to the nearest power of 2, so the space usage
95
        // may be a little larger than 3 bytes * NumCounters.
96
        NumCounters int64
97
        // MaxCost can be considered as the cache capacity, in whatever units you
98
        // choose to use.
99
        //
100
        // For example, if you want the cache to have a max capacity of 100MB, you
101
        // would set MaxCost to 100,000,000 and pass an item's number of bytes as
102
        // the `cost` parameter for calls to Set. If new items are accepted, the
103
        // eviction process will take care of making room for the new item and not
104
        // overflowing the MaxCost value.
105
        MaxCost int64
106
        // BufferItems determines the size of Get buffers.
107
        //
108
        // Unless you have a rare use case, using `64` as the BufferItems value
109
        // results in good performance.
110
        BufferItems int64
111
        // Metrics determines whether cache statistics are kept during the cache's
112
        // lifetime. There *is* some overhead to keeping statistics, so you should
113
        // only set this flag to true when testing or throughput performance isn't a
114
        // major factor.
115
        Metrics bool
116
        // OnEvict is called for every eviction and passes the hashed key, value,
117
        // and cost to the function.
118
        OnEvict func(item *Item)
119
        // OnReject is called for every rejection done via the policy.
120
        OnReject func(item *Item)
121
        // OnExit is called whenever a value is removed from cache. This can be
122
        // used to do manual memory deallocation. Would also be called on eviction
123
        // and rejection of the value.
124
        OnExit func(val interface{})
125
        // KeyToHash function is used to customize the key hashing algorithm.
126
        // Each key will be hashed using the provided function. If keyToHash value
127
        // is not set, the default keyToHash function is used.
128
        KeyToHash func(key interface{}) (uint64, uint64)
129
        // Cost evaluates a value and outputs a corresponding cost. This function
130
        // is ran after Set is called for a new item or an item update with a cost
131
        // param of 0.
132
        Cost func(value interface{}) int64
133
        // IgnoreInternalCost set to true indicates to the cache that the cost of
134
        // internally storing the value should be ignored. This is useful when the
135
        // cost passed to set is not using bytes as units. Keep in mind that setting
136
        // this to true will increase the memory usage.
137
        IgnoreInternalCost bool
138
        // TtlTickerDurationInSec set the value of time ticker for cleanup keys on ttl
139
        TtlTickerDurationInSec int64
140
}
141

142
type itemFlag byte
143

144
const (
145
        itemNew itemFlag = iota
146
        itemDelete
147
        itemUpdate
148
)
149

150
// Item is passed to setBuf so items can eventually be added to the cache.
151
type Item struct {
152
        flag       itemFlag
153
        Key        uint64
154
        Conflict   uint64
155
        Value      interface{}
156
        Cost       int64
157
        Expiration time.Time
158
        wg         *sync.WaitGroup
159
}
160

161
// NewCache returns a new Cache instance and any configuration errors, if any.
162
func NewCache(config *Config) (*Cache, error) {
139✔
163
        switch {
139✔
164
        case config.NumCounters == 0:
1✔
165
                return nil, errors.New("NumCounters can't be zero")
1✔
166
        case config.MaxCost == 0:
1✔
167
                return nil, errors.New("MaxCost can't be zero")
1✔
168
        case config.BufferItems == 0:
1✔
169
                return nil, errors.New("BufferItems can't be zero")
1✔
170
        case config.TtlTickerDurationInSec == 0:
136✔
171
                config.TtlTickerDurationInSec = bucketDurationSecs
136✔
172
        }
173
        policy := newPolicy(config.NumCounters, config.MaxCost)
136✔
174
        cache := &Cache{
136✔
175
                store:              newStore(),
136✔
176
                policy:             policy,
136✔
177
                getBuf:             newRingBuffer(policy, config.BufferItems),
136✔
178
                setBuf:             make(chan *Item, setBufSize),
136✔
179
                keyToHash:          config.KeyToHash,
136✔
180
                stop:               make(chan struct{}),
136✔
181
                cost:               config.Cost,
136✔
182
                ignoreInternalCost: config.IgnoreInternalCost,
136✔
183
                cleanupTicker:      time.NewTicker(time.Duration(config.TtlTickerDurationInSec) * time.Second / 2),
136✔
184
        }
136✔
185
        cache.onExit = func(val interface{}) {
86,642✔
186
                if config.OnExit != nil && val != nil {
126,506✔
187
                        config.OnExit(val)
40,000✔
188
                }
40,000✔
189
        }
190
        cache.onEvict = func(item *Item) {
38,621✔
191
                if config.OnEvict != nil {
38,491✔
192
                        config.OnEvict(item)
6✔
193
                }
6✔
194
                cache.onExit(item.Value)
38,485✔
195
        }
196
        cache.onReject = func(item *Item) {
9,898✔
197
                if config.OnReject != nil {
9,762✔
198
                        config.OnReject(item)
×
199
                }
×
200
                cache.onExit(item.Value)
9,762✔
201
        }
202
        if cache.keyToHash == nil {
271✔
203
                cache.keyToHash = z.KeyToHash
135✔
204
        }
135✔
205
        if config.Metrics {
266✔
206
                cache.collectMetrics()
130✔
207
        }
130✔
208
        // NOTE: benchmarks seem to show that performance decreases the more
209
        //       goroutines we have running cache.processItems(), so 1 should
210
        //       usually be sufficient
211
        go cache.processItems()
136✔
212
        return cache, nil
136✔
213
}
214

215
// Wait blocks until all buffered writes have been applied. This ensures a call to Set()
216
// will be visible to future calls to Get().
217
func (c *Cache) Wait() {
210✔
218
        if c == nil || c.isClosed {
210✔
219
                return
×
220
        }
×
221
        wg := &sync.WaitGroup{}
210✔
222
        wg.Add(1)
210✔
223
        c.setBuf <- &Item{wg: wg}
210✔
224
        wg.Wait()
210✔
225
}
226

227
// Get returns the value (if any) and a boolean representing whether the
228
// value was found or not. The value can be nil and the boolean can be true at
229
// the same time. Get will not return expired items.
230
func (c *Cache) Get(key interface{}) (interface{}, bool) {
153,654✔
231
        if c == nil || c.isClosed || key == nil {
153,657✔
232
                return nil, false
3✔
233
        }
3✔
234
        keyHash, conflictHash := c.keyToHash(key)
153,651✔
235
        c.getBuf.Push(keyHash)
153,651✔
236
        value, ok := c.store.Get(keyHash, conflictHash)
153,651✔
237
        if ok {
266,089✔
238
                c.Metrics.add(hit, keyHash, 1)
112,438✔
239
        } else {
153,651✔
240
                c.Metrics.add(miss, keyHash, 1)
41,213✔
241
        }
41,213✔
242
        return value, ok
153,651✔
243
}
244

245
// Set attempts to add the key-value item to the cache. If it returns false,
246
// then the Set was dropped and the key-value item isn't added to the cache. If
247
// it returns true, there's still a chance it could be dropped by the policy if
248
// its determined that the key-value item isn't worth keeping, but otherwise the
249
// item will be added and other items will be evicted in order to make room.
250
//
251
// To dynamically evaluate the items cost using the Config.Coster function, set
252
// the cost parameter to 0 and Coster will be ran when needed in order to find
253
// the items true cost.
254
func (c *Cache) Set(key, value interface{}, cost int64) bool {
62,767✔
255
        return c.SetWithTTL(key, value, cost, 0*time.Second)
62,767✔
256
}
62,767✔
257

258
// SetWithTTL works like Set but adds a key-value pair to the cache that will expire
259
// after the specified TTL (time to live) has passed. A zero value means the value never
260
// expires, which is identical to calling Set. A negative value is a no-op and the value
261
// is discarded.
262
func (c *Cache) SetWithTTL(key, value interface{}, cost int64, ttl time.Duration) bool {
82,790✔
263
        if c == nil || c.isClosed || key == nil {
82,793✔
264
                return false
3✔
265
        }
3✔
266

267
        var expiration time.Time
82,787✔
268
        switch {
82,787✔
269
        case ttl == 0:
62,768✔
270
                // No expiration.
62,768✔
271
                break
62,768✔
272
        case ttl < 0:
×
273
                // Treat this a no-op.
×
274
                return false
×
275
        default:
20,019✔
276
                expiration = time.Now().Add(ttl)
20,019✔
277
        }
278

279
        keyHash, conflictHash := c.keyToHash(key)
82,787✔
280
        i := &Item{
82,787✔
281
                flag:       itemNew,
82,787✔
282
                Key:        keyHash,
82,787✔
283
                Conflict:   conflictHash,
82,787✔
284
                Value:      value,
82,787✔
285
                Cost:       cost,
82,787✔
286
                Expiration: expiration,
82,787✔
287
        }
82,787✔
288
        // cost is eventually updated. The expiration must also be immediately updated
82,787✔
289
        // to prevent items from being prematurely removed from the map.
82,787✔
290
        if prev, ok := c.store.Update(i); ok {
114,154✔
291
                c.onExit(prev)
31,367✔
292
                i.flag = itemUpdate
31,367✔
293
        }
31,367✔
294
        // Attempt to send item to policy.
295
        select {
82,787✔
296
        case c.setBuf <- i:
82,644✔
297
                return true
82,644✔
298
        default:
143✔
299
                if i.flag == itemUpdate {
143✔
300
                        // Return true if this was an update operation since we've already
×
301
                        // updated the store. For all the other operations (set/delete), we
×
302
                        // return false which means the item was not inserted.
×
303
                        return true
×
304
                }
×
305
                c.Metrics.add(dropSets, keyHash, 1)
143✔
306
                return false
143✔
307
        }
308
}
309

310
// Del deletes the key-value item from the cache if it exists.
311
func (c *Cache) Del(key interface{}) {
3,968✔
312
        if c == nil || c.isClosed || key == nil {
3,971✔
313
                return
3✔
314
        }
3✔
315
        keyHash, conflictHash := c.keyToHash(key)
3,965✔
316
        // Delete immediately.
3,965✔
317
        _, prev := c.store.Del(keyHash, conflictHash)
3,965✔
318
        c.onExit(prev)
3,965✔
319
        // If we've set an item, it would be applied slightly later.
3,965✔
320
        // So we must push the same item to `setBuf` with the deletion flag.
3,965✔
321
        // This ensures that if a set is followed by a delete, it will be
3,965✔
322
        // applied in the correct order.
3,965✔
323
        c.setBuf <- &Item{
3,965✔
324
                flag:     itemDelete,
3,965✔
325
                Key:      keyHash,
3,965✔
326
                Conflict: conflictHash,
3,965✔
327
        }
3,965✔
328
}
329

330
// GetTTL returns the TTL for the specified key and a bool that is true if the
331
// item was found and is not expired.
332
func (c *Cache) GetTTL(key interface{}) (time.Duration, bool) {
5✔
333
        if c == nil || key == nil {
5✔
334
                return 0, false
×
335
        }
×
336

337
        keyHash, conflictHash := c.keyToHash(key)
5✔
338
        if _, ok := c.store.Get(keyHash, conflictHash); !ok {
8✔
339
                // not found
3✔
340
                return 0, false
3✔
341
        }
3✔
342

343
        expiration := c.store.Expiration(keyHash)
2✔
344
        if expiration.IsZero() {
3✔
345
                // found but no expiration
1✔
346
                return 0, true
1✔
347
        }
1✔
348

349
        if time.Now().After(expiration) {
1✔
350
                // found but expired
×
351
                return 0, false
×
352
        }
×
353

354
        return time.Until(expiration), true
1✔
355
}
356

357
// Close stops all goroutines and closes all channels.
358
func (c *Cache) Close() {
112✔
359
        if c == nil || c.isClosed {
115✔
360
                return
3✔
361
        }
3✔
362
        c.Clear()
109✔
363

109✔
364
        // Block until processItems goroutine is returned.
109✔
365
        c.stop <- struct{}{}
109✔
366
        close(c.stop)
109✔
367
        close(c.setBuf)
109✔
368
        c.policy.Close()
109✔
369
        c.isClosed = true
109✔
370
}
371

372
// Clear empties the hashmap and zeroes all policy counters. Note that this is
373
// not an atomic operation (but that shouldn't be a problem as it's assumed that
374
// Set/Get calls won't be occurring until after this).
375
func (c *Cache) Clear() {
124✔
376
        if c == nil || c.isClosed {
126✔
377
                return
2✔
378
        }
2✔
379
        // Block until processItems goroutine is returned.
380
        c.stop <- struct{}{}
122✔
381

122✔
382
        // Clear out the setBuf channel.
122✔
383
loop:
122✔
384
        for {
11,715✔
385
                select {
11,593✔
386
                case i := <-c.setBuf:
11,471✔
387
                        if i.wg != nil {
11,471✔
388
                                i.wg.Done()
×
389
                                continue
×
390
                        }
391
                        if i.flag != itemUpdate {
19,757✔
392
                                // In itemUpdate, the value is already set in the store.  So, no need to call
8,286✔
393
                                // onEvict here.
8,286✔
394
                                c.onEvict(i)
8,286✔
395
                        }
8,286✔
396
                default:
122✔
397
                        break loop
122✔
398
                }
399
        }
400

401
        // Clear value hashmap and policy data.
402
        c.policy.Clear()
122✔
403
        c.store.Clear(c.onEvict)
122✔
404
        // Only reset metrics if they're enabled.
122✔
405
        if c.Metrics != nil {
232✔
406
                c.Metrics.Clear()
110✔
407
        }
110✔
408
        // Restart processItems goroutine.
409
        go c.processItems()
122✔
410
}
411

412
// MaxCost returns the max cost of the cache.
413
func (c *Cache) MaxCost() int64 {
2✔
414
        if c == nil {
2✔
415
                return 0
×
416
        }
×
417
        return c.policy.MaxCost()
2✔
418
}
419

420
// UpdateMaxCost updates the maxCost of an existing cache.
421
func (c *Cache) UpdateMaxCost(maxCost int64) {
1✔
422
        if c == nil {
1✔
423
                return
×
424
        }
×
425
        c.policy.UpdateMaxCost(maxCost)
1✔
426
}
427

428
// processItems is ran by goroutines processing the Set buffer.
429
func (c *Cache) processItems() {
258✔
430
        startTs := make(map[uint64]time.Time)
258✔
431
        numToKeep := 100000 // TODO: Make this configurable via options.
258✔
432

258✔
433
        trackAdmission := func(key uint64) {
34,513✔
434
                if c.Metrics == nil {
34,263✔
435
                        return
8✔
436
                }
8✔
437
                startTs[key] = time.Now()
34,247✔
438
                if len(startTs) > numToKeep {
34,247✔
439
                        for k := range startTs {
×
440
                                if len(startTs) <= numToKeep {
×
441
                                        break
×
442
                                }
443
                                delete(startTs, k)
×
444
                        }
445
                }
446
        }
447
        onEvict := func(i *Item) {
27,522✔
448
                if ts, has := startTs[i.Key]; has {
54,526✔
449
                        c.Metrics.trackEviction(int64(time.Since(ts) / time.Second))
27,262✔
450
                        delete(startTs, i.Key)
27,262✔
451
                }
27,262✔
452
                if c.onEvict != nil {
54,529✔
453
                        c.onEvict(i)
27,265✔
454
                }
27,265✔
455
        }
456

457
        for {
77,005✔
458
                select {
76,747✔
459
                case i := <-c.setBuf:
75,345✔
460
                        if i.wg != nil {
75,555✔
461
                                i.wg.Done()
210✔
462
                                continue
210✔
463
                        }
464
                        // Calculate item cost value if new or update.
465
                        if i.Cost == 0 && c.cost != nil && i.flag != itemDelete {
75,139✔
466
                                i.Cost = c.cost(i.Value)
2✔
467
                        }
2✔
468
                        if !c.ignoreInternalCost {
150,133✔
469
                                // Add the cost of internally storing the object.
74,996✔
470
                                i.Cost += itemSize
74,996✔
471
                        }
74,996✔
472

473
                        switch i.flag {
75,137✔
474
                        case itemNew:
44,029✔
475
                                victims, added := c.policy.Add(i.Key, i.Cost)
44,029✔
476
                                if added {
78,296✔
477
                                        c.store.Set(i)
34,267✔
478
                                        c.Metrics.add(keyAdd, i.Key, 1)
34,267✔
479
                                        trackAdmission(i.Key)
34,267✔
480
                                } else {
44,031✔
481
                                        c.onReject(i)
9,764✔
482
                                }
9,764✔
483
                                for _, victim := range victims {
68,096✔
484
                                        victim.Conflict, victim.Value = c.store.Del(victim.Key, 0)
24,067✔
485
                                        onEvict(victim)
24,067✔
486
                                }
24,067✔
487

488
                        case itemUpdate:
28,183✔
489
                                c.policy.Update(i.Key, i.Cost)
28,183✔
490

491
                        case itemDelete:
2,928✔
492
                                c.policy.Del(i.Key) // Deals with metrics updates.
2,928✔
493
                                _, val := c.store.Del(i.Key, i.Conflict)
2,928✔
494
                                c.onExit(val)
2,928✔
495
                        }
496
                case <-c.cleanupTicker.C:
1,145✔
497
                        c.store.Cleanup(c.policy, onEvict)
1,145✔
498
                case <-c.stop:
232✔
499
                        return
232✔
500
                }
501
        }
502
}
503

504
// collectMetrics just creates a new *Metrics instance and adds the pointers
505
// to the cache and policy instances.
506
func (c *Cache) collectMetrics() {
130✔
507
        c.Metrics = newMetrics()
130✔
508
        c.policy.CollectMetrics(c.Metrics)
130✔
509
}
130✔
510

511
type metricType int
512

513
const (
514
        // The following 2 keep track of hits and misses.
515
        hit = iota
516
        miss
517
        // The following 3 keep track of number of keys added, updated and evicted.
518
        keyAdd
519
        keyUpdate
520
        keyEvict
521
        // The following 2 keep track of cost of keys added and evicted.
522
        costAdd
523
        costEvict
524
        // The following keep track of how many sets were dropped or rejected later.
525
        dropSets
526
        rejectSets
527
        // The following 2 keep track of how many gets were kept and dropped on the
528
        // floor.
529
        dropGets
530
        keepGets
531
        // This should be the final enum. Other enums should be set before this.
532
        doNotUse
533
)
534

535
func stringFor(t metricType) string {
12✔
536
        switch t {
12✔
537
        case hit:
1✔
538
                return "hit"
1✔
539
        case miss:
1✔
540
                return "miss"
1✔
541
        case keyAdd:
1✔
542
                return "keys-added"
1✔
543
        case keyUpdate:
1✔
544
                return "keys-updated"
1✔
545
        case keyEvict:
1✔
546
                return "keys-evicted"
1✔
547
        case costAdd:
1✔
548
                return "cost-added"
1✔
549
        case costEvict:
1✔
550
                return "cost-evicted"
1✔
551
        case dropSets:
1✔
552
                return "sets-dropped"
1✔
553
        case rejectSets:
1✔
554
                return "sets-rejected" // by policy.
1✔
555
        case dropGets:
1✔
556
                return "gets-dropped"
1✔
557
        case keepGets:
1✔
558
                return "gets-kept"
1✔
559
        default:
1✔
560
                return "unidentified"
1✔
561
        }
562
}
563

564
// Metrics is a snapshot of performance statistics for the lifetime of a cache instance.
565
type Metrics struct {
566
        all [doNotUse][]*uint64
567

568
        mu   sync.RWMutex
569
        life *z.HistogramData // Tracks the life expectancy of a key.
570
}
571

572
func newMetrics() *Metrics {
136✔
573
        s := &Metrics{
136✔
574
                life: z.NewHistogramData(z.HistogramBounds(1, 16)),
136✔
575
        }
136✔
576
        for i := 0; i < doNotUse; i++ {
1,632✔
577
                s.all[i] = make([]*uint64, 256)
1,496✔
578
                slice := s.all[i]
1,496✔
579
                for j := range slice {
384,472✔
580
                        slice[j] = new(uint64)
382,976✔
581
                }
382,976✔
582
        }
583
        return s
136✔
584
}
585

586
func (p *Metrics) add(t metricType, hash, delta uint64) {
326,229✔
587
        if p == nil {
326,304✔
588
                return
75✔
589
        }
75✔
590
        valp := p.all[t]
326,154✔
591
        // Avoid false sharing by padding at least 64 bytes of space between two
326,154✔
592
        // atomic counters which would be incremented.
326,154✔
593
        idx := (hash % 25) * 10
326,154✔
594
        atomic.AddUint64(valp[idx], delta)
326,154✔
595
}
596

597
func (p *Metrics) get(t metricType) uint64 {
95✔
598
        if p == nil {
105✔
599
                return 0
10✔
600
        }
10✔
601
        valp := p.all[t]
85✔
602
        var total uint64
85✔
603
        for i := range valp {
21,845✔
604
                total += atomic.LoadUint64(valp[i])
21,760✔
605
        }
21,760✔
606
        return total
85✔
607
}
608

609
// Hits is the number of Get calls where a value was found for the corresponding key.
610
func (p *Metrics) Hits() uint64 {
4✔
611
        return p.get(hit)
4✔
612
}
4✔
613

614
// Misses is the number of Get calls where a value was not found for the corresponding key.
615
func (p *Metrics) Misses() uint64 {
2✔
616
        return p.get(miss)
2✔
617
}
2✔
618

619
// KeysAdded is the total number of Set calls where a new key-value item was added.
620
func (p *Metrics) KeysAdded() uint64 {
5✔
621
        return p.get(keyAdd)
5✔
622
}
5✔
623

624
// KeysUpdated is the total number of Set calls where the value was updated.
625
func (p *Metrics) KeysUpdated() uint64 {
1✔
626
        return p.get(keyUpdate)
1✔
627
}
1✔
628

629
// KeysEvicted is the total number of keys evicted.
630
func (p *Metrics) KeysEvicted() uint64 {
2✔
631
        return p.get(keyEvict)
2✔
632
}
2✔
633

634
// CostAdded is the sum of costs that have been added (successful Set calls).
635
func (p *Metrics) CostAdded() uint64 {
21✔
636
        return p.get(costAdd)
21✔
637
}
21✔
638

639
// CostEvicted is the sum of all costs that have been evicted.
640
func (p *Metrics) CostEvicted() uint64 {
22✔
641
        return p.get(costEvict)
22✔
642
}
22✔
643

644
// SetsDropped is the number of Set calls that don't make it into internal
645
// buffers (due to contention or some other reason).
646
func (p *Metrics) SetsDropped() uint64 {
3✔
647
        return p.get(dropSets)
3✔
648
}
3✔
649

650
// SetsRejected is the number of Set calls rejected by the policy (TinyLFU).
651
func (p *Metrics) SetsRejected() uint64 {
2✔
652
        return p.get(rejectSets)
2✔
653
}
2✔
654

655
// GetsDropped is the number of Get counter increments that are dropped
656
// internally.
657
func (p *Metrics) GetsDropped() uint64 {
2✔
658
        return p.get(dropGets)
2✔
659
}
2✔
660

661
// GetsKept is the number of Get counter increments that are kept.
662
func (p *Metrics) GetsKept() uint64 {
2✔
663
        return p.get(keepGets)
2✔
664
}
2✔
665

666
// Ratio is the number of Hits over all accesses (Hits + Misses). This is the
667
// percentage of successful Get calls.
668
func (p *Metrics) Ratio() float64 {
9✔
669
        if p == nil {
10✔
670
                return 0.0
1✔
671
        }
1✔
672
        hits, misses := p.get(hit), p.get(miss)
8✔
673
        if hits == 0 && misses == 0 {
9✔
674
                return 0.0
1✔
675
        }
1✔
676
        return float64(hits) / float64(hits+misses)
7✔
677
}
678

679
func (p *Metrics) trackEviction(numSeconds int64) {
27,272✔
680
        if p == nil {
27,272✔
681
                return
×
682
        }
×
683
        p.mu.Lock()
27,272✔
684
        defer p.mu.Unlock()
27,272✔
685
        p.life.Update(numSeconds)
27,272✔
686
}
687

688
func (p *Metrics) LifeExpectancySeconds() *z.HistogramData {
×
689
        if p == nil {
×
690
                return nil
×
691
        }
×
692
        p.mu.RLock()
×
693
        defer p.mu.RUnlock()
×
694
        return p.life.Copy()
×
695
}
696

697
// Clear resets all the metrics.
698
func (p *Metrics) Clear() {
111✔
699
        if p == nil {
112✔
700
                return
1✔
701
        }
1✔
702
        for i := 0; i < doNotUse; i++ {
1,320✔
703
                for j := range p.all[i] {
310,970✔
704
                        atomic.StoreUint64(p.all[i][j], 0)
309,760✔
705
                }
309,760✔
706
        }
707
        p.mu.Lock()
110✔
708
        p.life = z.NewHistogramData(z.HistogramBounds(1, 16))
110✔
709
        p.mu.Unlock()
110✔
710
}
711

712
// String returns a string representation of the metrics.
713
func (p *Metrics) String() string {
2✔
714
        if p == nil {
3✔
715
                return ""
1✔
716
        }
1✔
717
        var buf bytes.Buffer
1✔
718
        for i := 0; i < doNotUse; i++ {
12✔
719
                t := metricType(i)
11✔
720
                fmt.Fprintf(&buf, "%s: %d ", stringFor(t), p.get(t))
11✔
721
        }
11✔
722
        fmt.Fprintf(&buf, "gets-total: %d ", p.get(hit)+p.get(miss))
1✔
723
        fmt.Fprintf(&buf, "hit-ratio: %.2f", p.Ratio())
1✔
724
        return buf.String()
1✔
725
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc