Skip to content

Commit e6d8a22

Browse files
fix: evict the limits cache once half the TTL has elapsed (#17337)
1 parent 6e89c1e commit e6d8a22

File tree

2 files changed

+79
-10
lines changed

2 files changed

+79
-10
lines changed

pkg/limits/frontend/cache.go

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,10 @@ func (i *item[V]) hasExpired(now time.Time) bool {
3232

3333
// TTLCache is a simple, thread-safe cache with a single per-cache TTL.
3434
type TTLCache[K comparable, V any] struct {
35-
items map[K]item[V]
36-
ttl time.Duration
37-
mu sync.RWMutex
35+
items map[K]item[V]
36+
ttl time.Duration
37+
lastEvictedAt time.Time
38+
mu sync.RWMutex
3839

3940
// Used for tests.
4041
clock quartz.Clock
@@ -73,7 +74,10 @@ func (c *TTLCache[K, V]) Set(key K, value V) {
7374
value: value,
7475
expiresAt: now.Add(c.ttl),
7576
}
76-
c.removeExpiredItems(now)
77+
if now.Sub(c.lastEvictedAt) > c.ttl/2 {
78+
c.lastEvictedAt = now
79+
c.evictExpired(now)
80+
}
7781
}
7882

7983
// Delete implements Cache.Delete.
@@ -90,8 +94,8 @@ func (c *TTLCache[K, V]) Reset() {
9094
c.items = make(map[K]item[V])
9195
}
9296

93-
// removeExpiredItems removes expired items.
94-
func (c *TTLCache[K, V]) removeExpiredItems(now time.Time) {
97+
// evictExpired evicts expired items.
98+
func (c *TTLCache[K, V]) evictExpired(now time.Time) {
9599
for key, item := range c.items {
96100
if item.hasExpired(now) {
97101
delete(c.items, key)

pkg/limits/frontend/cache_test.go

Lines changed: 69 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -106,25 +106,90 @@ func TestTTLCache_Reset(t *testing.T) {
106106
require.Equal(t, "qux", value)
107107
}
108108

109-
func TestTTLCache_RemoveExpiredItems(t *testing.T) {
109+
func TestTTLCache_EvictExpired(t *testing.T) {
110110
c := NewTTLCache[string, string](time.Minute)
111111
clock := quartz.NewMock(t)
112112
c.clock = clock
113113
c.Set("foo", "bar")
114114
_, ok := c.items["foo"]
115115
require.True(t, ok)
116-
// Advance the clock and update foo, it should not be removed.
116+
// Advance the clock and update foo, it should not be evicted as its
117+
// expiration time should be refreshed.
117118
clock.Advance(time.Minute)
118119
c.Set("foo", "bar")
119120
_, ok = c.items["foo"]
120121
require.True(t, ok)
121-
// Advance the clock again but this time set bar, foo should be removed.
122-
clock.Advance(time.Minute)
122+
eviction1 := clock.Now()
123+
require.Equal(t, eviction1, c.lastEvictedAt)
124+
// Advance the clock 15 seconds. Since 15 seconds is less than half
125+
// the TTL (30 seconds) since the last eviction, no eviction should
126+
// be run.
127+
clock.Advance(15 * time.Second)
123128
c.Set("bar", "baz")
124129
_, ok = c.items["foo"]
130+
require.True(t, ok)
131+
_, ok = c.items["bar"]
132+
require.True(t, ok)
133+
require.Equal(t, eviction1, c.lastEvictedAt)
134+
// Advance the clock 16 seconds. Since 31 seconds is more than half the TTL
135+
// since the last eviction, an eviction should be run, but no items should
136+
// be evicted.
137+
clock.Advance(16 * time.Second)
138+
c.Set("baz", "qux")
139+
_, ok = c.items["foo"]
140+
require.True(t, ok)
141+
_, ok = c.items["bar"]
142+
require.True(t, ok)
143+
_, ok = c.items["baz"]
144+
require.True(t, ok)
145+
eviction2 := clock.Now()
146+
require.Equal(t, eviction2, c.lastEvictedAt)
147+
// Advance the clock another 15 seconds. Again, since 15 seconds is less
148+
// than half the TTL (seconds) since the last eviction, no eviction should
149+
// be run.
150+
clock.Advance(15 * time.Second)
151+
c.Set("qux", "corge")
152+
_, ok = c.items["foo"]
153+
require.True(t, ok)
154+
_, ok = c.items["bar"]
155+
require.True(t, ok)
156+
_, ok = c.items["baz"]
157+
require.True(t, ok)
158+
_, ok = c.items["qux"]
159+
require.True(t, ok)
160+
require.Equal(t, eviction2, c.lastEvictedAt)
161+
// Advance the clock another 16 seconds. Since 31 seconds is more than
162+
// half the TTL since the last eviction, an eviction should be run and
163+
// this time foo should be evicted as it has expired.
164+
clock.Advance(16 * time.Second)
165+
c.Set("corge", "jorge")
166+
_, ok = c.items["foo"]
125167
require.False(t, ok)
126168
_, ok = c.items["bar"]
127169
require.True(t, ok)
170+
_, ok = c.items["baz"]
171+
require.True(t, ok)
172+
_, ok = c.items["qux"]
173+
require.True(t, ok)
174+
_, ok = c.items["corge"]
175+
require.True(t, ok)
176+
eviction3 := clock.Now()
177+
require.Equal(t, eviction3, c.lastEvictedAt)
178+
// Advance the clock one whole minute. All items should be expired.
179+
clock.Advance(time.Minute)
180+
c.Set("foo", "bar")
181+
_, ok = c.items["foo"]
182+
require.True(t, ok)
183+
_, ok = c.items["bar"]
184+
require.False(t, ok)
185+
_, ok = c.items["baz"]
186+
require.False(t, ok)
187+
_, ok = c.items["qux"]
188+
require.False(t, ok)
189+
_, ok = c.items["qux"]
190+
require.False(t, ok)
191+
eviction4 := clock.Now()
192+
require.Equal(t, eviction4, c.lastEvictedAt)
128193
}
129194

130195
func TestNopCache(t *testing.T) {

0 commit comments

Comments
 (0)