diff --git a/CHANGELOG b/CHANGELOG index 7f5db664..bdfd4e1f 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [2.0.0-rc.15] - 2022-02-18 +## Changes +* Added metric to track number of cache evictions which involve unexpired entries. + ## [2.0.0-rc.14] - 2022-02-17 ## Changes * Added performance optimizations to ensure batching behavior does not cause diff --git a/lrucache.go b/lrucache.go index c639b5c5..4e0632be 100644 --- a/lrucache.go +++ b/lrucache.go @@ -53,6 +53,10 @@ var accessMetric = prometheus.NewCounterVec(prometheus.CounterOpts{ Name: "gubernator_cache_access_count", Help: "Cache access counts. Label \"type\" = hit|miss.", }, []string{"type"}) +var unexpiredEvictionsMetric = prometheus.NewCounter(prometheus.CounterOpts{ + Name: "gubernator_unexpired_evictions_count", + Help: "Count the number of cache items which were evicted while unexpired.", +}) // New creates a new Cache with a maximum size. func NewLRUCache(maxSize int) *LRUCache { @@ -144,6 +148,12 @@ func (c *LRUCache) Remove(key string) { func (c *LRUCache) removeOldest() { ele := c.ll.Back() if ele != nil { + entry := ele.Value.(*CacheItem) + + if MillisecondNow() < entry.ExpireAt { + unexpiredEvictionsMetric.Add(1) + } + c.removeElement(ele) } } @@ -192,6 +202,7 @@ func (collector *LRUCacheCollector) AddCache(cache Cache) { func (collector *LRUCacheCollector) Describe(ch chan<- *prometheus.Desc) { sizeMetric.Describe(ch) accessMetric.Describe(ch) + unexpiredEvictionsMetric.Describe(ch) } // Collect fetches metric counts and gauges from the cache @@ -199,6 +210,7 @@ func (collector *LRUCacheCollector) Collect(ch chan<- prometheus.Metric) { sizeMetric.Set(collector.getSize()) sizeMetric.Collect(ch) accessMetric.Collect(ch) + unexpiredEvictionsMetric.Collect(ch) } func (collector *LRUCacheCollector) getSize() float64 { diff --git a/lrucache_test.go b/lrucache_test.go index a188e6d7..600ff156 100644 --- a/lrucache_test.go +++ b/lrucache_test.go @@ -17,6 +17,7 @@ limitations under the License. package gubernator_test import ( + "fmt" "math/rand" "strconv" "sync" @@ -28,6 +29,8 @@ import ( "github.com/prometheus/client_golang/prometheus" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + dto "github.com/prometheus/client_model/go" ) func TestLRUCache(t *testing.T) { @@ -332,6 +335,97 @@ func TestLRUCache(t *testing.T) { launchWg.Done() doneWg.Wait() }) + + t.Run("Check gubernator_unexpired_evictions_count metric is not incremented when expired item is evicted", func(t *testing.T) { + defer clock.Freeze(clock.Now()).Unfreeze() + + promRegister := prometheus.NewRegistry() + + // The LRU cache for storing rate limits. + cacheCollector := gubernator.NewLRUCacheCollector() + err := promRegister.Register(cacheCollector) + require.NoError(t, err) + + cache := gubernator.NewLRUCache(10) + cacheCollector.AddCache(cache) + + // fill cache with short duration cache items + for i := 0; i < 10; i++ { + cache.Add(&gubernator.CacheItem{ + Algorithm: gubernator.Algorithm_LEAKY_BUCKET, + Key: fmt.Sprintf("short-expiry-%d", i), + Value: "bar", + ExpireAt: clock.Now().Add(5 * time.Minute).UnixMilli(), + }) + } + + // jump forward in time to expire all short duration keys + clock.Advance(6 * time.Minute) + + // add a new cache item to force eviction + cache.Add(&gubernator.CacheItem{ + Algorithm: gubernator.Algorithm_LEAKY_BUCKET, + Key: "evict1", + Value: "bar", + ExpireAt: clock.Now().Add(1 * time.Hour).UnixMilli(), + }) + + collChan := make(chan prometheus.Metric, 64) + cacheCollector.Collect(collChan) + // Check metrics to verify evicted cache key is expired + <-collChan + <-collChan + <-collChan + m := <-collChan // gubernator_unexpired_evictions_count + met := new(dto.Metric) + m.Write(met) + assert.Contains(t, m.Desc().String(), "gubernator_unexpired_evictions_count") + assert.Equal(t, 0, int(*met.Counter.Value)) + }) + + t.Run("Check gubernator_unexpired_evictions_count metric is incremented when unexpired item is evicted", func(t *testing.T) { + defer clock.Freeze(clock.Now()).Unfreeze() + + promRegister := prometheus.NewRegistry() + + // The LRU cache for storing rate limits. + cacheCollector := gubernator.NewLRUCacheCollector() + err := promRegister.Register(cacheCollector) + require.NoError(t, err) + + cache := gubernator.NewLRUCache(10) + cacheCollector.AddCache(cache) + + // fill cache with long duration cache items + for i := 0; i < 10; i++ { + cache.Add(&gubernator.CacheItem{ + Algorithm: gubernator.Algorithm_LEAKY_BUCKET, + Key: fmt.Sprintf("long-expiry-%d", i), + Value: "bar", + ExpireAt: clock.Now().Add(1 * time.Hour).UnixMilli(), + }) + } + + // add a new cache item to force eviction + cache.Add(&gubernator.CacheItem{ + Algorithm: gubernator.Algorithm_LEAKY_BUCKET, + Key: "evict2", + Value: "bar", + ExpireAt: clock.Now().Add(1 * time.Hour).UnixMilli(), + }) + + // Check metrics to verify evicted cache key is *NOT* expired + collChan := make(chan prometheus.Metric, 64) + cacheCollector.Collect(collChan) + <-collChan + <-collChan + <-collChan + m := <-collChan // gubernator_unexpired_evictions_count + met := new(dto.Metric) + m.Write(met) + assert.Contains(t, m.Desc().String(), "gubernator_unexpired_evictions_count") + assert.Equal(t, 1, int(*met.Counter.Value)) + }) } func BenchmarkLRUCache(b *testing.B) { diff --git a/version b/version index b58ddb78..4639736a 100644 --- a/version +++ b/version @@ -1 +1 @@ -v2.0.0-rc.14 +v2.0.0-rc.15