Skip to content

Commit

Permalink
ConcurrentLfu time-based expiry (#516)
Browse files Browse the repository at this point in the history
* 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
bitfaster authored May 8, 2024
1 parent 434a7be commit de72842
Show file tree
Hide file tree
Showing 18 changed files with 2,147 additions and 340 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
<PackageReference Include="FluentAssertions" Version="6.12.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
<PackageReference Include="Moq" Version="4.20.69" />
<PackageReference Include="ObjectLayoutInspector" Version="0.1.4" />
<PackageReference Include="xunit" Version="2.8.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
<PrivateAssets>all</PrivateAssets>
Expand Down
41 changes: 41 additions & 0 deletions BitFaster.Caching.UnitTests/ExpireAfterAccessTests.cs
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);
}
}
}
42 changes: 42 additions & 0 deletions BitFaster.Caching.UnitTests/ExpireAfterWriteTests.cs
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 BitFaster.Caching.UnitTests/Lfu/ConcurrentLfuCoreTests.cs
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 BitFaster.Caching.UnitTests/Lfu/ConcurrentTLfuSoakTests.cs
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
}
}
}
Loading

0 comments on commit de72842

Please sign in to comment.