Skip to content

Commit 01fd0e3

Browse files
authored
fix: Force evict entries with the same expiration date (#28)
1 parent d368220 commit 01fd0e3

File tree

3 files changed

+150
-2
lines changed

3 files changed

+150
-2
lines changed

cache_test.go

+80
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,86 @@ func TestForcedEvictions(t *testing.T) {
174174
}
175175
}
176176

177+
func TestForceEvictAllEntries(t *testing.T) {
178+
t.Parallel()
179+
capacity := 100
180+
numShards := 1
181+
ttl := time.Hour
182+
evictionpercentage := 100
183+
clock := sturdyc.NewTestClock(time.Now())
184+
c := sturdyc.New[string](capacity, numShards, ttl, evictionpercentage,
185+
sturdyc.WithClock(clock),
186+
)
187+
188+
// Now we're going to write 101 records to the cache which should
189+
// exceed its capacity and trigger a forced eviction.
190+
for i := 0; i < 101; i++ {
191+
c.Set(strconv.Itoa(i), strconv.Itoa(i))
192+
}
193+
194+
// When the eviction is triggered by the 100th write, we expect the cache to
195+
// be emptied. Therefore, the 101th write should mean that the size is now 1.
196+
if c.Size() != 1 {
197+
t.Errorf("expected cache size to be 0, got %d", c.Size())
198+
}
199+
}
200+
201+
func TestForceEvictionSameTime(t *testing.T) {
202+
t.Parallel()
203+
capacity := 100
204+
numShards := 2
205+
ttl := time.Hour
206+
evictionpercentage := 50
207+
clock := sturdyc.NewTestClock(time.Now())
208+
c := sturdyc.New[string](capacity, numShards, ttl, evictionpercentage,
209+
sturdyc.WithClock(clock),
210+
)
211+
212+
// Now we're going to write 1000 records to the cache which should
213+
// exceed its capacity and trigger a couple of forced evictions.
214+
for i := 0; i < 1000; i++ {
215+
c.Set(strconv.Itoa(i), strconv.Itoa(i))
216+
}
217+
218+
// Assert that even though we're writing 1000
219+
// records we never exceed the capacity of 100.
220+
if c.Size() > 100 {
221+
t.Errorf("exceeded the cache size of 100, got %d", c.Size())
222+
}
223+
}
224+
225+
func TestForceEvictionTwoDifferentTimes(t *testing.T) {
226+
t.Parallel()
227+
capacity := 100
228+
numShards := 1
229+
ttl := time.Hour
230+
evictionpercentage := 10
231+
clock := sturdyc.NewTestClock(time.Now())
232+
c := sturdyc.New[string](capacity, numShards, ttl, evictionpercentage,
233+
sturdyc.WithClock(clock),
234+
)
235+
236+
// We're going to write 50 records, then move the clock forward
237+
// and write another 50 to reach the capacity of the cache.
238+
for i := 0; i < 50; i++ {
239+
c.Set(strconv.Itoa(i), strconv.Itoa(i))
240+
}
241+
clock.Add(time.Hour)
242+
for i := 0; i < 50; i++ {
243+
c.Set(strconv.Itoa(i+50), strconv.Itoa(i+50))
244+
}
245+
246+
// At this point, the cache should be at its capacity so
247+
// adding another item should trigger a forced eviction.
248+
// Given our eviction percentage of 10%, we expect the
249+
// cache to first remove 10 items, and then write this
250+
// record afterwards.
251+
c.Set(strconv.Itoa(100), strconv.Itoa(100))
252+
if c.Size() != 91 {
253+
t.Errorf("expected cache size to be 91, got %d", c.Size())
254+
}
255+
}
256+
177257
func TestDisablingForcedEvictionMakesSetANoop(t *testing.T) {
178258
t.Parallel()
179259

quickselect_test.go

+49
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,55 @@ func TestCutoff(t *testing.T) {
6262
}
6363
}
6464

65+
func TestCutOffSameTime(t *testing.T) {
66+
t.Parallel()
67+
now := time.Now()
68+
timestamps := make([]time.Time, 0, 100)
69+
for i := 0; i < 100; i++ {
70+
timestamps = append(timestamps, now)
71+
}
72+
73+
// Given that we have a list where all the timestamps are the same, we
74+
// should get that same timestamp back for every percentile.
75+
cutoffOne := sturdyc.FindCutoff(timestamps, 0.1)
76+
cutoffTwo := sturdyc.FindCutoff(timestamps, 0.3)
77+
cutoffThree := sturdyc.FindCutoff(timestamps, 0.5)
78+
if cutoffOne != now {
79+
t.Errorf("expected cutoff to be %v, got %v", now, cutoffOne)
80+
}
81+
if cutoffTwo != now {
82+
t.Errorf("expected cutoff to be %v, got %v", now, cutoffTwo)
83+
}
84+
if cutoffThree != now {
85+
t.Errorf("expected cutoff to be %v, got %v", now, cutoffThree)
86+
}
87+
}
88+
89+
func TestCutOffTwoTimes(t *testing.T) {
90+
t.Parallel()
91+
timestamps := make([]time.Time, 0, 100)
92+
93+
firstTime := time.Now()
94+
for i := 0; i < 50; i++ {
95+
timestamps = append(timestamps, firstTime)
96+
}
97+
98+
secondTime := time.Now().Add(time.Second)
99+
for i := 0; i < 50; i++ {
100+
timestamps = append(timestamps, secondTime)
101+
}
102+
103+
firstCutoff := sturdyc.FindCutoff(timestamps, 0.49)
104+
if firstCutoff != firstTime {
105+
t.Errorf("expected cutoff to be %v, got %v", firstTime, firstCutoff)
106+
}
107+
108+
secondCutoff := sturdyc.FindCutoff(timestamps, 0.51)
109+
if secondCutoff != secondTime {
110+
t.Errorf("expected cutoff to be %v, got %v", secondTime, secondCutoff)
111+
}
112+
}
113+
65114
func TestReturnsEmptyTimeIfArgumentsAreInvalid(t *testing.T) {
66115
t.Parallel()
67116

shard.go

+21-2
Original file line numberDiff line numberDiff line change
@@ -64,17 +64,36 @@ func (s *shard[T]) evictExpired() {
6464
// based on the expiration time. Should be called with a lock.
6565
func (s *shard[T]) forceEvict() {
6666
s.reportForcedEviction()
67+
68+
// Check if we should evict all entries.
69+
if s.evictionPercentage == 100 {
70+
s.entries = make(map[string]*entry[T])
71+
s.reportEntriesEvicted(len(s.entries))
72+
return
73+
}
74+
6775
expirationTimes := make([]time.Time, 0, len(s.entries))
6876
for _, e := range s.entries {
6977
expirationTimes = append(expirationTimes, e.expiresAt)
7078
}
7179

72-
cutoff := FindCutoff(expirationTimes, float64(s.evictionPercentage)/100)
80+
// We could have a lumpy distribution of expiration times. As an example, we
81+
// might have 100 entries in the cache but only 2 unique expiration times. In
82+
// order to not over-evict when trying to remove 10%, we'll have to keep
83+
// track of the number of entries that we've evicted.
84+
percentage := float64(s.evictionPercentage) / 100
85+
cutoff := FindCutoff(expirationTimes, percentage)
86+
entriesToEvict := int(float64(len(expirationTimes)) * percentage)
7387
entriesEvicted := 0
7488
for key, e := range s.entries {
75-
if e.expiresAt.Before(cutoff) {
89+
// Here we're essentially saying: if e.expiresAt <= cutoff.
90+
if !e.expiresAt.After(cutoff) {
7691
delete(s.entries, key)
7792
entriesEvicted++
93+
94+
if entriesEvicted == entriesToEvict {
95+
break
96+
}
7897
}
7998
}
8099
s.reportEntriesEvicted(entriesEvicted)

0 commit comments

Comments
 (0)