Skip to content

Commit b3891ea

Browse files
authored
ConcurrentLru item based time eviction policy (#467)
Implement a time-based eviction policy where the caller can specify the time to expire per item.
1 parent 530368e commit b3891ea

24 files changed

+1702
-591
lines changed

BitFaster.Caching.UnitTests/CachePolicyTests.cs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ public void WhenCtorFieldsAreAssigned()
1717
cp.Eviction.Value.Should().Be(eviction.Object);
1818
cp.ExpireAfterWrite.Value.Should().Be(expire.Object);
1919
cp.ExpireAfterAccess.HasValue.Should().BeFalse();
20+
cp.ExpireAfter.HasValue.Should().BeFalse();
2021
}
2122

2223
[Fact]
@@ -40,7 +41,16 @@ public void TryTrimWhenExpireAfterWriteReturnsTrue()
4041
public void TryTrimWhenExpireAfterAccessReturnsTrue()
4142
{
4243
var expire = new Mock<ITimePolicy>();
43-
var cp = new CachePolicy(Optional<IBoundedPolicy>.None(), Optional<ITimePolicy>.None(), new Optional<ITimePolicy>(expire.Object));
44+
var cp = new CachePolicy(Optional<IBoundedPolicy>.None(), Optional<ITimePolicy>.None(), new Optional<ITimePolicy>(expire.Object), Optional<IDiscreteTimePolicy>.None());
45+
46+
cp.TryTrimExpired().Should().BeTrue();
47+
}
48+
49+
[Fact]
50+
public void TryTrimWhenExpireAfterReturnsTrue()
51+
{
52+
var expire = new Mock<IDiscreteTimePolicy>();
53+
var cp = new CachePolicy(Optional<IBoundedPolicy>.None(), Optional<ITimePolicy>.None(), Optional<ITimePolicy>.None(), new Optional<IDiscreteTimePolicy>(expire.Object));
4454

4555
cp.TryTrimExpired().Should().BeTrue();
4656
}
Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Runtime.InteropServices;
4+
using BitFaster.Caching.Lru;
5+
using FluentAssertions;
6+
using Xunit;
7+
8+
namespace BitFaster.Caching.UnitTests.Lru
9+
{
10+
public class ConcurrentLruAfterDiscreteTests
11+
{
12+
private readonly ICapacityPartition capacity = new EqualCapacityPartition(9);
13+
private ICache<int, string> lru;
14+
15+
private ValueFactory valueFactory = new ValueFactory();
16+
private TestExpiryCalculator<int, string> expiryCalculator = new TestExpiryCalculator<int, string>();
17+
18+
private List<ItemRemovedEventArgs<int, string>> removedItems = new List<ItemRemovedEventArgs<int, string>>();
19+
20+
// on MacOS time measurement seems to be less stable, give longer pause
21+
private int ttlWaitMlutiplier = RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 8 : 2;
22+
23+
private static readonly TimeSpan delta = TimeSpan.FromMilliseconds(20);
24+
25+
private void OnLruItemRemoved(object sender, ItemRemovedEventArgs<int, string> e)
26+
{
27+
removedItems.Add(e);
28+
}
29+
30+
public ConcurrentLruAfterDiscreteTests()
31+
{
32+
lru = new ConcurrentLruBuilder<int, string>()
33+
.WithCapacity(capacity)
34+
.WithExpireAfter(expiryCalculator)
35+
.Build();
36+
}
37+
38+
[Fact]
39+
public void WhenKeyIsWrongTypeTryGetTimeToExpireIsFalse()
40+
{
41+
lru.Policy.ExpireAfter.Value.TryGetTimeToExpire("foo", out _).Should().BeFalse();
42+
}
43+
44+
[Fact]
45+
public void WhenKeyDoesNotExistTryGetTimeToExpireIsFalse()
46+
{
47+
lru.Policy.ExpireAfter.Value.TryGetTimeToExpire(1, out _).Should().BeFalse();
48+
}
49+
50+
[Fact]
51+
public void WhenKeyExistsTryGetTimeToExpireReturnsExpiryTime()
52+
{
53+
lru.GetOrAdd(1, k => "1");
54+
lru.Policy.ExpireAfter.Value.TryGetTimeToExpire(1, out var expiry).Should().BeTrue();
55+
expiry.Should().BeCloseTo(TestExpiryCalculator<int, string>.DefaultTimeToExpire, delta);
56+
}
57+
58+
[Fact]
59+
public void WhenItemIsExpiredItIsRemoved()
60+
{
61+
Timed.Execute(
62+
lru,
63+
lru =>
64+
{
65+
lru.GetOrAdd(1, valueFactory.Create);
66+
return lru;
67+
},
68+
TestExpiryCalculator<int, string>.DefaultTimeToExpire.MultiplyBy(ttlWaitMlutiplier),
69+
lru =>
70+
{
71+
lru.TryGet(1, out var value).Should().BeFalse();
72+
}
73+
);
74+
}
75+
76+
[Fact]
77+
public void WhenItemIsUpdatedTtlIsExtended()
78+
{
79+
Timed.Execute(
80+
lru,
81+
lru =>
82+
{
83+
lru.GetOrAdd(1, valueFactory.Create);
84+
return lru;
85+
},
86+
TestExpiryCalculator<int, string>.DefaultTimeToExpire.MultiplyBy(ttlWaitMlutiplier),
87+
lru =>
88+
{
89+
lru.TryUpdate(1, "3");
90+
lru.TryGet(1, out var value).Should().BeTrue();
91+
}
92+
);
93+
}
94+
95+
[Fact]
96+
public void WhenItemIsReadTtlIsExtended()
97+
{
98+
expiryCalculator.ExpireAfterCreate = (_, _) => TimeSpan.FromMilliseconds(100);
99+
100+
var lru = new ConcurrentLruBuilder<int, string>()
101+
.WithCapacity(capacity)
102+
.WithExpireAfter(expiryCalculator)
103+
.Build();
104+
105+
// execute the method to ensure it is always jitted
106+
lru.GetOrAdd(-1, valueFactory.Create);
107+
lru.GetOrAdd(-2, valueFactory.Create);
108+
lru.GetOrAdd(-3, valueFactory.Create);
109+
110+
Timed.Execute(
111+
lru,
112+
lru =>
113+
{
114+
lru.GetOrAdd(1, valueFactory.Create);
115+
return lru;
116+
},
117+
TimeSpan.FromMilliseconds(50),
118+
lru =>
119+
{
120+
lru.TryGet(1, out _).Should().BeTrue($"First");
121+
},
122+
TimeSpan.FromMilliseconds(75),
123+
lru =>
124+
{
125+
lru.TryGet(1, out var value).Should().BeTrue($"Second");
126+
}
127+
);
128+
}
129+
130+
[Fact]
131+
public void WhenValueEvictedItemRemovedEventIsFired()
132+
{
133+
expiryCalculator.ExpireAfterCreate = (_, _) => TimeSpan.FromSeconds(10);
134+
135+
var lruEvents = new ConcurrentLruBuilder<int, string>()
136+
.WithCapacity(new EqualCapacityPartition(6))
137+
.WithExpireAfter(expiryCalculator)
138+
.WithMetrics()
139+
.Build();
140+
141+
lruEvents.Events.Value.ItemRemoved += OnLruItemRemoved;
142+
143+
// First 6 adds
144+
// hot[6, 5], warm[2, 1], cold[4, 3]
145+
// =>
146+
// hot[8, 7], warm[1, 0], cold[6, 5], evicted[4, 3]
147+
for (int i = 0; i < 8; i++)
148+
{
149+
lruEvents.GetOrAdd(i + 1, i => $"{i + 1}");
150+
}
151+
152+
removedItems.Count.Should().Be(2);
153+
154+
removedItems[0].Key.Should().Be(1);
155+
removedItems[0].Value.Should().Be("2");
156+
removedItems[0].Reason.Should().Be(ItemRemovedReason.Evicted);
157+
158+
removedItems[1].Key.Should().Be(4);
159+
removedItems[1].Value.Should().Be("5");
160+
removedItems[1].Reason.Should().Be(ItemRemovedReason.Evicted);
161+
}
162+
163+
[Fact]
164+
public void WhenItemsAreExpiredExpireRemovesExpiredItems()
165+
{
166+
Timed.Execute(
167+
lru,
168+
lru =>
169+
{
170+
lru.AddOrUpdate(1, "1");
171+
lru.AddOrUpdate(2, "2");
172+
lru.AddOrUpdate(3, "3");
173+
lru.GetOrAdd(1, valueFactory.Create);
174+
lru.GetOrAdd(2, valueFactory.Create);
175+
lru.GetOrAdd(3, valueFactory.Create);
176+
177+
lru.AddOrUpdate(4, "4");
178+
lru.AddOrUpdate(5, "5");
179+
lru.AddOrUpdate(6, "6");
180+
181+
lru.AddOrUpdate(7, "7");
182+
lru.AddOrUpdate(8, "8");
183+
lru.AddOrUpdate(9, "9");
184+
185+
return lru;
186+
},
187+
TestExpiryCalculator<int, string>.DefaultTimeToExpire.MultiplyBy(ttlWaitMlutiplier),
188+
lru =>
189+
{
190+
lru.Policy.ExpireAfter.Value.TrimExpired();
191+
192+
lru.Count.Should().Be(0);
193+
}
194+
);
195+
}
196+
197+
[Fact]
198+
public void WhenCacheHasExpiredAndFreshItemsExpireRemovesOnlyExpiredItems()
199+
{
200+
Timed.Execute(
201+
lru,
202+
lru =>
203+
{
204+
lru.AddOrUpdate(1, "1");
205+
lru.AddOrUpdate(2, "2");
206+
lru.AddOrUpdate(3, "3");
207+
208+
lru.AddOrUpdate(4, "4");
209+
lru.AddOrUpdate(5, "5");
210+
lru.AddOrUpdate(6, "6");
211+
212+
return lru;
213+
},
214+
TestExpiryCalculator<int, string>.DefaultTimeToExpire.MultiplyBy(ttlWaitMlutiplier),
215+
lru =>
216+
{
217+
lru.GetOrAdd(1, valueFactory.Create);
218+
lru.GetOrAdd(2, valueFactory.Create);
219+
lru.GetOrAdd(3, valueFactory.Create);
220+
221+
lru.Policy.ExpireAfter.Value.TrimExpired();
222+
223+
lru.Count.Should().Be(3);
224+
}
225+
);
226+
}
227+
228+
[Fact]
229+
public void WhenItemsAreExpiredTrimRemovesExpiredItems()
230+
{
231+
Timed.Execute(
232+
lru,
233+
lru =>
234+
{
235+
lru.AddOrUpdate(1, "1");
236+
lru.AddOrUpdate(2, "2");
237+
lru.AddOrUpdate(3, "3");
238+
239+
return lru;
240+
},
241+
TestExpiryCalculator<int, string>.DefaultTimeToExpire.MultiplyBy(ttlWaitMlutiplier),
242+
lru =>
243+
{
244+
lru.Policy.Eviction.Value.Trim(1);
245+
246+
lru.Count.Should().Be(0);
247+
}
248+
);
249+
}
250+
}
251+
}

0 commit comments

Comments
 (0)