-
Notifications
You must be signed in to change notification settings - Fork 31
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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 ---------
- Loading branch information
Showing
18 changed files
with
2,147 additions
and
340 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
using System; | ||
using FluentAssertions; | ||
using Xunit; | ||
|
||
namespace BitFaster.Caching.UnitTests | ||
{ | ||
public class ExpireAfterAccessTests | ||
{ | ||
private readonly Duration expiry = Duration.FromMinutes(1); | ||
private readonly ExpireAfterAccess<int, int> expiryCalculator; | ||
|
||
public ExpireAfterAccessTests() | ||
{ | ||
expiryCalculator = new(expiry.ToTimeSpan()); | ||
} | ||
|
||
[Fact] | ||
public void TimeToExpireReturnsCtorArg() | ||
{ | ||
expiryCalculator.TimeToExpire.Should().Be(expiry.ToTimeSpan()); | ||
} | ||
|
||
[Fact] | ||
public void AfterCreateReturnsTimeToExpire() | ||
{ | ||
expiryCalculator.GetExpireAfterCreate(1, 2).Should().Be(expiry); | ||
} | ||
|
||
[Fact] | ||
public void AfteReadReturnsTimeToExpire() | ||
{ | ||
expiryCalculator.GetExpireAfterRead(1, 2, Duration.SinceEpoch()).Should().Be(expiry); | ||
} | ||
|
||
[Fact] | ||
public void AfteUpdateReturnsTimeToExpire() | ||
{ | ||
expiryCalculator.GetExpireAfterUpdate(1, 2, Duration.SinceEpoch()).Should().Be(expiry); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
using System; | ||
using FluentAssertions; | ||
using Xunit; | ||
|
||
namespace BitFaster.Caching.UnitTests | ||
{ | ||
public class ExpireAfterWriteTests | ||
{ | ||
private readonly Duration expiry = Duration.FromMinutes(1); | ||
private readonly ExpireAfterWrite<int, int> expiryCalculator; | ||
|
||
public ExpireAfterWriteTests() | ||
{ | ||
expiryCalculator = new(expiry.ToTimeSpan()); | ||
} | ||
|
||
[Fact] | ||
public void TimeToExpireReturnsCtorArg() | ||
{ | ||
expiryCalculator.TimeToExpire.Should().Be(expiry.ToTimeSpan()); | ||
} | ||
|
||
[Fact] | ||
public void AfterCreateReturnsTimeToExpire() | ||
{ | ||
expiryCalculator.GetExpireAfterCreate(1, 2).Should().Be(expiry); | ||
} | ||
|
||
[Fact] | ||
public void AfteReadReturnsCurrentTimeToExpire() | ||
{ | ||
var current = new Duration(123); | ||
expiryCalculator.GetExpireAfterRead(1, 2, current).Should().Be(current); | ||
} | ||
|
||
[Fact] | ||
public void AfteUpdateReturnsTimeToExpire() | ||
{ | ||
expiryCalculator.GetExpireAfterUpdate(1, 2, Duration.SinceEpoch()).Should().Be(expiry); | ||
} | ||
} | ||
} |
202 changes: 202 additions & 0 deletions
202
BitFaster.Caching.UnitTests/Lfu/ConcurrentLfuCoreTests.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,202 @@ | ||
using System; | ||
using System.Collections; | ||
using System.Collections.Generic; | ||
using System.Threading.Tasks; | ||
using BitFaster.Caching.Lfu; | ||
using FluentAssertions; | ||
using Xunit; | ||
|
||
namespace BitFaster.Caching.UnitTests.Lfu | ||
{ | ||
public abstract class ConcurrentLfuCoreTests | ||
{ | ||
protected readonly TimeSpan timeToLive = TimeSpan.FromMilliseconds(200); | ||
protected readonly int capacity = 20; | ||
|
||
private ConcurrentLfuTests.ValueFactory valueFactory = new(); | ||
|
||
private ICache<int, int> lfu; | ||
|
||
public abstract ICache<K, V> Create<K,V>(); | ||
public abstract void DoMaintenance<K, V>(ICache<K, V> cache); | ||
|
||
public ConcurrentLfuCoreTests() | ||
{ | ||
lfu = Create<int, int>(); | ||
} | ||
|
||
[Fact] | ||
public void EvictionPolicyCapacityReturnsCapacity() | ||
{ | ||
lfu.Policy.Eviction.Value.Capacity.Should().Be(capacity); | ||
} | ||
|
||
[Fact] | ||
public void WhenKeyIsRequestedItIsCreatedAndCached() | ||
{ | ||
var result1 = lfu.GetOrAdd(1, valueFactory.Create); | ||
var result2 = lfu.GetOrAdd(1, valueFactory.Create); | ||
|
||
valueFactory.timesCalled.Should().Be(1); | ||
result1.Should().Be(result2); | ||
} | ||
#if NETCOREAPP3_0_OR_GREATER | ||
[Fact] | ||
public void WhenKeyIsRequestedWithArgItIsCreatedAndCached() | ||
{ | ||
var result1 = lfu.GetOrAdd(1, valueFactory.Create, 9); | ||
var result2 = lfu.GetOrAdd(1, valueFactory.Create, 17); | ||
|
||
valueFactory.timesCalled.Should().Be(1); | ||
result1.Should().Be(result2); | ||
} | ||
#endif | ||
[Fact] | ||
public async Task WhenKeyIsRequesteItIsCreatedAndCachedAsync() | ||
{ | ||
var asyncLfu = lfu as IAsyncCache<int, int>; | ||
var result1 = await asyncLfu.GetOrAddAsync(1, valueFactory.CreateAsync); | ||
var result2 = await asyncLfu.GetOrAddAsync(1, valueFactory.CreateAsync); | ||
|
||
valueFactory.timesCalled.Should().Be(1); | ||
result1.Should().Be(result2); | ||
} | ||
|
||
#if NETCOREAPP3_0_OR_GREATER | ||
[Fact] | ||
public async Task WhenKeyIsRequestedWithArgItIsCreatedAndCachedAsync() | ||
{ | ||
var asyncLfu = lfu as IAsyncCache<int, int>; | ||
var result1 = await asyncLfu.GetOrAddAsync(1, valueFactory.CreateAsync, 9); | ||
var result2 = await asyncLfu.GetOrAddAsync(1, valueFactory.CreateAsync, 17); | ||
|
||
valueFactory.timesCalled.Should().Be(1); | ||
result1.Should().Be(result2); | ||
} | ||
#endif | ||
|
||
[Fact] | ||
public void WhenItemIsUpdatedItIsUpdated() | ||
{ | ||
lfu.GetOrAdd(1, k => k); | ||
lfu.AddOrUpdate(1, 2); | ||
|
||
lfu.TryGet(1, out var value).Should().BeTrue(); | ||
value.Should().Be(2); | ||
} | ||
|
||
[Fact] | ||
public void WhenItemDoesNotExistUpdatedAddsItem() | ||
{ | ||
lfu.AddOrUpdate(1, 2); | ||
|
||
lfu.TryGet(1, out var value).Should().BeTrue(); | ||
value.Should().Be(2); | ||
} | ||
|
||
|
||
[Fact] | ||
public void WhenKeyExistsTryRemoveRemovesItem() | ||
{ | ||
lfu.GetOrAdd(1, k => k); | ||
|
||
lfu.TryRemove(1).Should().BeTrue(); | ||
lfu.TryGet(1, out _).Should().BeFalse(); | ||
} | ||
|
||
#if NETCOREAPP3_0_OR_GREATER | ||
[Fact] | ||
public void WhenKeyExistsTryRemoveReturnsValue() | ||
{ | ||
lfu.GetOrAdd(1, valueFactory.Create); | ||
|
||
lfu.TryRemove(1, out var value).Should().BeTrue(); | ||
value.Should().Be(1); | ||
} | ||
|
||
[Fact] | ||
public void WhenItemExistsTryRemoveRemovesItem() | ||
{ | ||
lfu.GetOrAdd(1, k => k); | ||
|
||
lfu.TryRemove(new KeyValuePair<int, int>(1, 1)).Should().BeTrue(); | ||
lfu.TryGet(1, out _).Should().BeFalse(); | ||
} | ||
|
||
[Fact] | ||
public void WhenItemDoesntMatchTryRemoveDoesNotRemove() | ||
{ | ||
lfu.GetOrAdd(1, k => k); | ||
|
||
lfu.TryRemove(new KeyValuePair<int, int>(1, 2)).Should().BeFalse(); | ||
lfu.TryGet(1, out var value).Should().BeTrue(); | ||
} | ||
#endif | ||
|
||
[Fact] | ||
public void WhenClearedCacheIsEmpty() | ||
{ | ||
lfu.GetOrAdd(1, k => k); | ||
lfu.GetOrAdd(2, k => k); | ||
|
||
lfu.Clear(); | ||
|
||
lfu.Count.Should().Be(0); | ||
lfu.TryGet(1, out var _).Should().BeFalse(); | ||
} | ||
|
||
[Fact] | ||
public void TrimRemovesNItems() | ||
{ | ||
for (int i = 0; i < 25; i++) | ||
{ | ||
lfu.GetOrAdd(i, k => k); | ||
} | ||
DoMaintenance<int, int>(lfu); | ||
|
||
lfu.Count.Should().Be(20); | ||
|
||
lfu.Policy.Eviction.Value.Trim(5); | ||
DoMaintenance<int, int>(lfu); | ||
|
||
lfu.Count.Should().Be(15); | ||
} | ||
|
||
[Fact] | ||
public void WhenItemsAddedGenericEnumerateContainsKvps() | ||
{ | ||
lfu.GetOrAdd(1, k => k); | ||
lfu.GetOrAdd(2, k => k); | ||
|
||
var enumerator = lfu.GetEnumerator(); | ||
enumerator.MoveNext().Should().BeTrue(); | ||
enumerator.Current.Should().Be(new KeyValuePair<int, int>(1, 1)); | ||
enumerator.MoveNext().Should().BeTrue(); | ||
enumerator.Current.Should().Be(new KeyValuePair<int, int>(2, 2)); | ||
} | ||
|
||
[Fact] | ||
public void WhenItemsAddedEnumerateContainsKvps() | ||
{ | ||
lfu.GetOrAdd(1, k => k); | ||
lfu.GetOrAdd(2, k => k); | ||
|
||
var enumerable = (IEnumerable)lfu; | ||
enumerable.Should().BeEquivalentTo(new[] { new KeyValuePair<int, int>(1, 1), new KeyValuePair<int, int>(2, 2) }); | ||
} | ||
} | ||
|
||
public class ConcurrentTLfuWrapperTests : ConcurrentLfuCoreTests | ||
{ | ||
public override ICache<K, V> Create<K,V>() | ||
{ | ||
return new ConcurrentTLfu<K, V>(capacity, new ExpireAfterWrite<K, V>(timeToLive)); | ||
} | ||
|
||
public override void DoMaintenance<K, V>(ICache<K, V> cache) | ||
{ | ||
var tlfu = cache as ConcurrentTLfu<K, V>; | ||
tlfu?.DoMaintenance(); | ||
} | ||
} | ||
} |
44 changes: 44 additions & 0 deletions
44
BitFaster.Caching.UnitTests/Lfu/ConcurrentTLfuSoakTests.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Linq; | ||
using System.Text; | ||
using System.Threading.Tasks; | ||
using BitFaster.Caching.Lfu; | ||
using Xunit; | ||
using Xunit.Abstractions; | ||
|
||
namespace BitFaster.Caching.UnitTests.Lfu | ||
{ | ||
[Collection("Soak")] | ||
public class ConcurrentTLfuSoakTests | ||
{ | ||
private const int soakIterations = 10; | ||
private const int threads = 4; | ||
private const int loopIterations = 100_000; | ||
|
||
private readonly ITestOutputHelper output; | ||
|
||
public ConcurrentTLfuSoakTests(ITestOutputHelper testOutputHelper) | ||
{ | ||
this.output = testOutputHelper; | ||
} | ||
|
||
[Theory] | ||
[Repeat(soakIterations)] | ||
public async Task GetOrAddWithExpiry(int iteration) | ||
{ | ||
var lfu = new ConcurrentTLfu<int, string>(20, new ExpireAfterWrite<int, string>(TimeSpan.FromMilliseconds(10))); | ||
|
||
await Threaded.RunAsync(threads, async () => { | ||
for (int i = 0; i < loopIterations; i++) | ||
{ | ||
await lfu.GetOrAddAsync(i + 1, i => Task.FromResult(i.ToString())); | ||
} | ||
}); | ||
|
||
this.output.WriteLine($"iteration {iteration} keys={string.Join(" ", lfu.Keys)}"); | ||
|
||
// TODO: integrity check, including TimerWheel | ||
} | ||
} | ||
} |
Oops, something went wrong.