Skip to content

Commit de72842

Browse files
authored
ConcurrentLfu time-based expiry (#516)
* wheel+nodes * fix typo * 64bit * +ConcurrentLfuCore * undo bitops * undo bitops * node * cleanup merge * simplify generics * outline tests * more tests * comments * use Duration, cleanup * rough end to end * nullability static analysis * schedule * assert wheel pos * port all tests * test e2e * policy * cleanup * test coverage * explicit interface impl * rem comment * mem layout ---------
1 parent 434a7be commit de72842

18 files changed

+2147
-340
lines changed

BitFaster.Caching.UnitTests/BitFaster.Caching.UnitTests.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
<PackageReference Include="FluentAssertions" Version="6.12.0" />
1414
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
1515
<PackageReference Include="Moq" Version="4.20.69" />
16+
<PackageReference Include="ObjectLayoutInspector" Version="0.1.4" />
1617
<PackageReference Include="xunit" Version="2.8.0" />
1718
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
1819
<PrivateAssets>all</PrivateAssets>
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
using System;
2+
using FluentAssertions;
3+
using Xunit;
4+
5+
namespace BitFaster.Caching.UnitTests
6+
{
7+
public class ExpireAfterAccessTests
8+
{
9+
private readonly Duration expiry = Duration.FromMinutes(1);
10+
private readonly ExpireAfterAccess<int, int> expiryCalculator;
11+
12+
public ExpireAfterAccessTests()
13+
{
14+
expiryCalculator = new(expiry.ToTimeSpan());
15+
}
16+
17+
[Fact]
18+
public void TimeToExpireReturnsCtorArg()
19+
{
20+
expiryCalculator.TimeToExpire.Should().Be(expiry.ToTimeSpan());
21+
}
22+
23+
[Fact]
24+
public void AfterCreateReturnsTimeToExpire()
25+
{
26+
expiryCalculator.GetExpireAfterCreate(1, 2).Should().Be(expiry);
27+
}
28+
29+
[Fact]
30+
public void AfteReadReturnsTimeToExpire()
31+
{
32+
expiryCalculator.GetExpireAfterRead(1, 2, Duration.SinceEpoch()).Should().Be(expiry);
33+
}
34+
35+
[Fact]
36+
public void AfteUpdateReturnsTimeToExpire()
37+
{
38+
expiryCalculator.GetExpireAfterUpdate(1, 2, Duration.SinceEpoch()).Should().Be(expiry);
39+
}
40+
}
41+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
using System;
2+
using FluentAssertions;
3+
using Xunit;
4+
5+
namespace BitFaster.Caching.UnitTests
6+
{
7+
public class ExpireAfterWriteTests
8+
{
9+
private readonly Duration expiry = Duration.FromMinutes(1);
10+
private readonly ExpireAfterWrite<int, int> expiryCalculator;
11+
12+
public ExpireAfterWriteTests()
13+
{
14+
expiryCalculator = new(expiry.ToTimeSpan());
15+
}
16+
17+
[Fact]
18+
public void TimeToExpireReturnsCtorArg()
19+
{
20+
expiryCalculator.TimeToExpire.Should().Be(expiry.ToTimeSpan());
21+
}
22+
23+
[Fact]
24+
public void AfterCreateReturnsTimeToExpire()
25+
{
26+
expiryCalculator.GetExpireAfterCreate(1, 2).Should().Be(expiry);
27+
}
28+
29+
[Fact]
30+
public void AfteReadReturnsCurrentTimeToExpire()
31+
{
32+
var current = new Duration(123);
33+
expiryCalculator.GetExpireAfterRead(1, 2, current).Should().Be(current);
34+
}
35+
36+
[Fact]
37+
public void AfteUpdateReturnsTimeToExpire()
38+
{
39+
expiryCalculator.GetExpireAfterUpdate(1, 2, Duration.SinceEpoch()).Should().Be(expiry);
40+
}
41+
}
42+
}
Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
using System;
2+
using System.Collections;
3+
using System.Collections.Generic;
4+
using System.Threading.Tasks;
5+
using BitFaster.Caching.Lfu;
6+
using FluentAssertions;
7+
using Xunit;
8+
9+
namespace BitFaster.Caching.UnitTests.Lfu
10+
{
11+
public abstract class ConcurrentLfuCoreTests
12+
{
13+
protected readonly TimeSpan timeToLive = TimeSpan.FromMilliseconds(200);
14+
protected readonly int capacity = 20;
15+
16+
private ConcurrentLfuTests.ValueFactory valueFactory = new();
17+
18+
private ICache<int, int> lfu;
19+
20+
public abstract ICache<K, V> Create<K,V>();
21+
public abstract void DoMaintenance<K, V>(ICache<K, V> cache);
22+
23+
public ConcurrentLfuCoreTests()
24+
{
25+
lfu = Create<int, int>();
26+
}
27+
28+
[Fact]
29+
public void EvictionPolicyCapacityReturnsCapacity()
30+
{
31+
lfu.Policy.Eviction.Value.Capacity.Should().Be(capacity);
32+
}
33+
34+
[Fact]
35+
public void WhenKeyIsRequestedItIsCreatedAndCached()
36+
{
37+
var result1 = lfu.GetOrAdd(1, valueFactory.Create);
38+
var result2 = lfu.GetOrAdd(1, valueFactory.Create);
39+
40+
valueFactory.timesCalled.Should().Be(1);
41+
result1.Should().Be(result2);
42+
}
43+
#if NETCOREAPP3_0_OR_GREATER
44+
[Fact]
45+
public void WhenKeyIsRequestedWithArgItIsCreatedAndCached()
46+
{
47+
var result1 = lfu.GetOrAdd(1, valueFactory.Create, 9);
48+
var result2 = lfu.GetOrAdd(1, valueFactory.Create, 17);
49+
50+
valueFactory.timesCalled.Should().Be(1);
51+
result1.Should().Be(result2);
52+
}
53+
#endif
54+
[Fact]
55+
public async Task WhenKeyIsRequesteItIsCreatedAndCachedAsync()
56+
{
57+
var asyncLfu = lfu as IAsyncCache<int, int>;
58+
var result1 = await asyncLfu.GetOrAddAsync(1, valueFactory.CreateAsync);
59+
var result2 = await asyncLfu.GetOrAddAsync(1, valueFactory.CreateAsync);
60+
61+
valueFactory.timesCalled.Should().Be(1);
62+
result1.Should().Be(result2);
63+
}
64+
65+
#if NETCOREAPP3_0_OR_GREATER
66+
[Fact]
67+
public async Task WhenKeyIsRequestedWithArgItIsCreatedAndCachedAsync()
68+
{
69+
var asyncLfu = lfu as IAsyncCache<int, int>;
70+
var result1 = await asyncLfu.GetOrAddAsync(1, valueFactory.CreateAsync, 9);
71+
var result2 = await asyncLfu.GetOrAddAsync(1, valueFactory.CreateAsync, 17);
72+
73+
valueFactory.timesCalled.Should().Be(1);
74+
result1.Should().Be(result2);
75+
}
76+
#endif
77+
78+
[Fact]
79+
public void WhenItemIsUpdatedItIsUpdated()
80+
{
81+
lfu.GetOrAdd(1, k => k);
82+
lfu.AddOrUpdate(1, 2);
83+
84+
lfu.TryGet(1, out var value).Should().BeTrue();
85+
value.Should().Be(2);
86+
}
87+
88+
[Fact]
89+
public void WhenItemDoesNotExistUpdatedAddsItem()
90+
{
91+
lfu.AddOrUpdate(1, 2);
92+
93+
lfu.TryGet(1, out var value).Should().BeTrue();
94+
value.Should().Be(2);
95+
}
96+
97+
98+
[Fact]
99+
public void WhenKeyExistsTryRemoveRemovesItem()
100+
{
101+
lfu.GetOrAdd(1, k => k);
102+
103+
lfu.TryRemove(1).Should().BeTrue();
104+
lfu.TryGet(1, out _).Should().BeFalse();
105+
}
106+
107+
#if NETCOREAPP3_0_OR_GREATER
108+
[Fact]
109+
public void WhenKeyExistsTryRemoveReturnsValue()
110+
{
111+
lfu.GetOrAdd(1, valueFactory.Create);
112+
113+
lfu.TryRemove(1, out var value).Should().BeTrue();
114+
value.Should().Be(1);
115+
}
116+
117+
[Fact]
118+
public void WhenItemExistsTryRemoveRemovesItem()
119+
{
120+
lfu.GetOrAdd(1, k => k);
121+
122+
lfu.TryRemove(new KeyValuePair<int, int>(1, 1)).Should().BeTrue();
123+
lfu.TryGet(1, out _).Should().BeFalse();
124+
}
125+
126+
[Fact]
127+
public void WhenItemDoesntMatchTryRemoveDoesNotRemove()
128+
{
129+
lfu.GetOrAdd(1, k => k);
130+
131+
lfu.TryRemove(new KeyValuePair<int, int>(1, 2)).Should().BeFalse();
132+
lfu.TryGet(1, out var value).Should().BeTrue();
133+
}
134+
#endif
135+
136+
[Fact]
137+
public void WhenClearedCacheIsEmpty()
138+
{
139+
lfu.GetOrAdd(1, k => k);
140+
lfu.GetOrAdd(2, k => k);
141+
142+
lfu.Clear();
143+
144+
lfu.Count.Should().Be(0);
145+
lfu.TryGet(1, out var _).Should().BeFalse();
146+
}
147+
148+
[Fact]
149+
public void TrimRemovesNItems()
150+
{
151+
for (int i = 0; i < 25; i++)
152+
{
153+
lfu.GetOrAdd(i, k => k);
154+
}
155+
DoMaintenance<int, int>(lfu);
156+
157+
lfu.Count.Should().Be(20);
158+
159+
lfu.Policy.Eviction.Value.Trim(5);
160+
DoMaintenance<int, int>(lfu);
161+
162+
lfu.Count.Should().Be(15);
163+
}
164+
165+
[Fact]
166+
public void WhenItemsAddedGenericEnumerateContainsKvps()
167+
{
168+
lfu.GetOrAdd(1, k => k);
169+
lfu.GetOrAdd(2, k => k);
170+
171+
var enumerator = lfu.GetEnumerator();
172+
enumerator.MoveNext().Should().BeTrue();
173+
enumerator.Current.Should().Be(new KeyValuePair<int, int>(1, 1));
174+
enumerator.MoveNext().Should().BeTrue();
175+
enumerator.Current.Should().Be(new KeyValuePair<int, int>(2, 2));
176+
}
177+
178+
[Fact]
179+
public void WhenItemsAddedEnumerateContainsKvps()
180+
{
181+
lfu.GetOrAdd(1, k => k);
182+
lfu.GetOrAdd(2, k => k);
183+
184+
var enumerable = (IEnumerable)lfu;
185+
enumerable.Should().BeEquivalentTo(new[] { new KeyValuePair<int, int>(1, 1), new KeyValuePair<int, int>(2, 2) });
186+
}
187+
}
188+
189+
public class ConcurrentTLfuWrapperTests : ConcurrentLfuCoreTests
190+
{
191+
public override ICache<K, V> Create<K,V>()
192+
{
193+
return new ConcurrentTLfu<K, V>(capacity, new ExpireAfterWrite<K, V>(timeToLive));
194+
}
195+
196+
public override void DoMaintenance<K, V>(ICache<K, V> cache)
197+
{
198+
var tlfu = cache as ConcurrentTLfu<K, V>;
199+
tlfu?.DoMaintenance();
200+
}
201+
}
202+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Text;
5+
using System.Threading.Tasks;
6+
using BitFaster.Caching.Lfu;
7+
using Xunit;
8+
using Xunit.Abstractions;
9+
10+
namespace BitFaster.Caching.UnitTests.Lfu
11+
{
12+
[Collection("Soak")]
13+
public class ConcurrentTLfuSoakTests
14+
{
15+
private const int soakIterations = 10;
16+
private const int threads = 4;
17+
private const int loopIterations = 100_000;
18+
19+
private readonly ITestOutputHelper output;
20+
21+
public ConcurrentTLfuSoakTests(ITestOutputHelper testOutputHelper)
22+
{
23+
this.output = testOutputHelper;
24+
}
25+
26+
[Theory]
27+
[Repeat(soakIterations)]
28+
public async Task GetOrAddWithExpiry(int iteration)
29+
{
30+
var lfu = new ConcurrentTLfu<int, string>(20, new ExpireAfterWrite<int, string>(TimeSpan.FromMilliseconds(10)));
31+
32+
await Threaded.RunAsync(threads, async () => {
33+
for (int i = 0; i < loopIterations; i++)
34+
{
35+
await lfu.GetOrAddAsync(i + 1, i => Task.FromResult(i.ToString()));
36+
}
37+
});
38+
39+
this.output.WriteLine($"iteration {iteration} keys={string.Join(" ", lfu.Keys)}");
40+
41+
// TODO: integrity check, including TimerWheel
42+
}
43+
}
44+
}

0 commit comments

Comments
 (0)