Skip to content

Commit ba6ddc0

Browse files
authored
ConcurrentLru expire after access eviction policy (#464)
Implement an expire after access eviction policy with a fixed time to live. This replicates the sliding window expiration provided by MemoryCache and the expireAfterAccess policy provided by Caffeine.
1 parent 4782d2e commit ba6ddc0

File tree

13 files changed

+790
-10
lines changed

13 files changed

+790
-10
lines changed

BitFaster.Caching.Benchmarks/Lru/LruJustGetOrAdd.cs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ public class LruJustGetOrAdd
4646
private static readonly FastConcurrentTLru<int, int> fastConcurrentTLru = new FastConcurrentTLru<int, int>(8, 9, EqualityComparer<int>.Default, TimeSpan.FromMinutes(1));
4747

4848
private static readonly ICache<int, int> atomicFastLru = new ConcurrentLruBuilder<int, int>().WithConcurrencyLevel(8).WithCapacity(9).WithAtomicGetOrAdd().Build();
49+
private static readonly ICache<int, int> lruAfterAccess = new ConcurrentLruBuilder<int, int>().WithConcurrencyLevel(8).WithCapacity(9).WithExpireAfterAccess(TimeSpan.FromMinutes(10)).Build();
4950

5051
private static readonly BackgroundThreadScheduler background = new BackgroundThreadScheduler();
5152
private static readonly ConcurrentLfu<int, int> concurrentLfu = new ConcurrentLfu<int, int>(1, 9, background, EqualityComparer<int>.Default);
@@ -103,14 +104,21 @@ public void FastConcurrentTLru()
103104
{
104105
Func<int, int> func = x => x;
105106
fastConcurrentTLru.GetOrAdd(1, func);
107+
}
108+
109+
[Benchmark()]
110+
public void FastConcLruAfter()
111+
{
112+
Func<int, int> func = x => x;
113+
lruAfterAccess.GetOrAdd(1, func);
106114
}
107115

108116
[Benchmark()]
109117
public void ConcurrentTLru()
110118
{
111119
Func<int, int> func = x => x;
112120
concurrentTlru.GetOrAdd(1, func);
113-
}
121+
}
114122

115123
[Benchmark()]
116124
public void ConcurrentLfu()
Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
using FluentAssertions;
2+
using BitFaster.Caching.Lru;
3+
using System;
4+
using System.Threading.Tasks;
5+
using Xunit;
6+
7+
#if NETFRAMEWORK
8+
using System.Diagnostics;
9+
#endif
10+
11+
namespace BitFaster.Caching.UnitTests.Lru
12+
{
13+
public class AfterAccessLongTicksPolicyTests
14+
{
15+
private readonly AfterAccessLongTicksPolicy<int, int> policy = new AfterAccessLongTicksPolicy<int, int>(TimeSpan.FromSeconds(10));
16+
17+
[Fact]
18+
public void WhenTtlIsTimeSpanMaxThrow()
19+
{
20+
Action constructor = () => { new AfterAccessLongTicksPolicy<int, int>(TimeSpan.MaxValue); };
21+
22+
constructor.Should().Throw<ArgumentOutOfRangeException>();
23+
}
24+
25+
[Fact]
26+
public void WhenTtlIsZeroThrow()
27+
{
28+
Action constructor = () => { new AfterAccessLongTicksPolicy<int, int>(TimeSpan.Zero); };
29+
30+
constructor.Should().Throw<ArgumentOutOfRangeException>();
31+
}
32+
33+
[Fact]
34+
public void WhenTtlIsMaxSetAsMax()
35+
{
36+
#if NETFRAMEWORK
37+
var maxRepresentable = TimeSpan.FromTicks((long)(long.MaxValue / 100.0d)) - TimeSpan.FromTicks(10);
38+
#else
39+
var maxRepresentable = Time.MaxRepresentable;
40+
#endif
41+
var policy = new AfterAccessLongTicksPolicy<int, int>(maxRepresentable);
42+
policy.TimeToLive.Should().BeCloseTo(maxRepresentable, TimeSpan.FromTicks(20));
43+
}
44+
45+
[Fact]
46+
public void TimeToLiveShouldBeTenSecs()
47+
{
48+
this.policy.TimeToLive.Should().Be(TimeSpan.FromSeconds(10));
49+
}
50+
51+
[Fact]
52+
public void CreateItemInitializesKeyAndValue()
53+
{
54+
var item = this.policy.CreateItem(1, 2);
55+
56+
item.Key.Should().Be(1);
57+
item.Value.Should().Be(2);
58+
}
59+
60+
[Fact]
61+
public void CreateItemInitializesTimestampToNow()
62+
{
63+
var item = this.policy.CreateItem(1, 2);
64+
65+
#if NETFRAMEWORK
66+
var expected = Stopwatch.GetTimestamp();
67+
ulong epsilon = (ulong)(TimeSpan.FromMilliseconds(20).TotalSeconds * Stopwatch.Frequency);
68+
#else
69+
var expected = Environment.TickCount64;
70+
ulong epsilon = 20;
71+
#endif
72+
item.TickCount.Should().BeCloseTo(expected, epsilon);
73+
}
74+
75+
[Fact]
76+
public void TouchUpdatesItemWasAccessed()
77+
{
78+
var item = this.policy.CreateItem(1, 2);
79+
item.WasAccessed = false;
80+
81+
this.policy.Touch(item);
82+
83+
item.WasAccessed.Should().BeTrue();
84+
}
85+
86+
[Fact]
87+
public async Task TouchUpdatesTicksCount()
88+
{
89+
var item = this.policy.CreateItem(1, 2);
90+
var tc = item.TickCount;
91+
await Task.Delay(TimeSpan.FromMilliseconds(1));
92+
93+
this.policy.ShouldDiscard(item); // set the time in the policy
94+
this.policy.Touch(item);
95+
96+
item.TickCount.Should().BeGreaterThan(tc);
97+
}
98+
99+
[Fact]
100+
public async Task UpdateUpdatesTickCount()
101+
{
102+
var item = this.policy.CreateItem(1, 2);
103+
var tc = item.TickCount;
104+
105+
await Task.Delay(TimeSpan.FromMilliseconds(1));
106+
107+
this.policy.Update(item);
108+
109+
item.TickCount.Should().BeGreaterThan(tc);
110+
}
111+
112+
[Fact]
113+
public void WhenItemIsExpiredShouldDiscardIsTrue()
114+
{
115+
var item = this.policy.CreateItem(1, 2);
116+
item.TickCount = Environment.TickCount - (int)TimeSpan.FromSeconds(11).ToEnvTick64();
117+
118+
this.policy.ShouldDiscard(item).Should().BeTrue();
119+
}
120+
121+
[Fact]
122+
public void WhenItemIsNotExpiredShouldDiscardIsFalse()
123+
{
124+
var item = this.policy.CreateItem(1, 2);
125+
126+
#if NETFRAMEWORK
127+
item.TickCount = Stopwatch.GetTimestamp() - StopwatchTickConverter.ToTicks(TimeSpan.FromSeconds(9));
128+
#else
129+
item.TickCount = Environment.TickCount - (int)TimeSpan.FromSeconds(9).ToEnvTick64();
130+
#endif
131+
132+
this.policy.ShouldDiscard(item).Should().BeFalse();
133+
}
134+
135+
[Fact]
136+
public void CanDiscardIsTrue()
137+
{
138+
this.policy.CanDiscard().Should().BeTrue();
139+
}
140+
141+
[Theory]
142+
[InlineData(false, true, ItemDestination.Remove)]
143+
[InlineData(true, true, ItemDestination.Remove)]
144+
[InlineData(true, false, ItemDestination.Warm)]
145+
[InlineData(false, false, ItemDestination.Cold)]
146+
public void RouteHot(bool wasAccessed, bool isExpired, ItemDestination expectedDestination)
147+
{
148+
var item = CreateItem(wasAccessed, isExpired);
149+
150+
this.policy.RouteHot(item).Should().Be(expectedDestination);
151+
}
152+
153+
[Theory]
154+
[InlineData(false, true, ItemDestination.Remove)]
155+
[InlineData(true, true, ItemDestination.Remove)]
156+
[InlineData(true, false, ItemDestination.Warm)]
157+
[InlineData(false, false, ItemDestination.Cold)]
158+
public void RouteWarm(bool wasAccessed, bool isExpired, ItemDestination expectedDestination)
159+
{
160+
var item = CreateItem(wasAccessed, isExpired);
161+
162+
this.policy.RouteWarm(item).Should().Be(expectedDestination);
163+
}
164+
165+
[Theory]
166+
[InlineData(false, true, ItemDestination.Remove)]
167+
[InlineData(true, true, ItemDestination.Remove)]
168+
[InlineData(true, false, ItemDestination.Warm)]
169+
[InlineData(false, false, ItemDestination.Remove)]
170+
public void RouteCold(bool wasAccessed, bool isExpired, ItemDestination expectedDestination)
171+
{
172+
var item = CreateItem(wasAccessed, isExpired);
173+
174+
this.policy.RouteCold(item).Should().Be(expectedDestination);
175+
}
176+
177+
private LongTickCountLruItem<int, int> CreateItem(bool wasAccessed, bool isExpired)
178+
{
179+
var item = this.policy.CreateItem(1, 2);
180+
181+
item.WasAccessed = wasAccessed;
182+
183+
if (isExpired)
184+
{
185+
#if NETFRAMEWORK
186+
item.TickCount = Stopwatch.GetTimestamp() - StopwatchTickConverter.ToTicks(TimeSpan.FromSeconds(11));
187+
#else
188+
item.TickCount = Environment.TickCount - TimeSpan.FromSeconds(11).ToEnvTick64();
189+
#endif
190+
}
191+
192+
return item;
193+
}
194+
}
195+
}

0 commit comments

Comments
 (0)