Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using BitFaster.Caching.Atomic;
using BitFaster.Caching.Lru;
using FluentAssertions;
using Xunit;

namespace BitFaster.Caching.UnitTests.Atomic
{
[Collection("Soak")]
public class AtomicFactoryScopedAsyncCacheSoakTests
{
private const int capacity = 6;
private const int threadCount = 4;
private const int soakIterations = 10;
private const int loopIterations = 100_000;

#if NET9_0_OR_GREATER
[Theory]
[Repeat(soakIterations)]
public async Task ScopedGetOrAddAsyncAlternateLifetimeIsAlwaysAlive(int _)
{
var cache = new AtomicFactoryScopedAsyncCache<string, Disposable>(new ConcurrentLru<string, ScopedAsyncAtomicFactory<string, Disposable>>(1, capacity, StringComparer.Ordinal));
var alternate = cache.GetAsyncAlternateLookup<ReadOnlySpan<char>>();

var run = Threaded.RunAsync(threadCount, async _ =>
{
var key = new char[8];

for (int i = 0; i < loopIterations; i++)
{
(i + 1).TryFormat(key, out int written);

using var lifetime = await alternate.ScopedGetOrAddAsync(key.AsSpan().Slice(0, written), static k => Task.FromResult(new Scoped<Disposable>(new Disposable(int.Parse(k)))));
lifetime.Value.IsDisposed.Should().BeFalse($"ref count {lifetime.ReferenceCount}");
}
});

await run;
}
#endif
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,150 @@ public void ComparerReturnsConfiguredComparer()

cache.Comparer.Should().BeSameAs(comparer);
}

[Fact]
public async Task TryGetAsyncAlternateLookupCompatibleComparerReturnsLookup()
{
var cache = new AtomicFactoryScopedAsyncCache<string, Disposable>(new ConcurrentLru<string, ScopedAsyncAtomicFactory<string, Disposable>>(1, capacity, StringComparer.Ordinal));
using var lifetime = await cache.ScopedGetOrAddAsync("42", _ => Task.FromResult(new Scoped<Disposable>(new Disposable(42))));
ReadOnlySpan<char> key = "42";

cache.TryGetAsyncAlternateLookup<ReadOnlySpan<char>>(out var alternate).Should().BeTrue();
alternate.ScopedTryGet(key, out var alternateLifetime).Should().BeTrue();
alternateLifetime.Value.State.Should().Be(42);
alternateLifetime.Dispose();
}

[Fact]
public void GetAsyncAlternateLookupIncompatibleComparerThrowsInvalidOperationException()
{
var cache = new AtomicFactoryScopedAsyncCache<string, Disposable>(new ConcurrentLru<string, ScopedAsyncAtomicFactory<string, Disposable>>(1, capacity, StringComparer.Ordinal));

Action act = () => cache.GetAsyncAlternateLookup<int>();

act.Should().Throw<InvalidOperationException>().WithMessage("Incompatible comparer");
cache.TryGetAsyncAlternateLookup<int>(out var alternate).Should().BeFalse();
alternate.Should().BeNull();
}

[Fact]
public async Task TryRemoveExistingKeyReturnsActualKey()
{
var cache = new AtomicFactoryScopedAsyncCache<string, Disposable>(new ConcurrentLru<string, ScopedAsyncAtomicFactory<string, Disposable>>(1, capacity, StringComparer.Ordinal));
using var lifetime = await cache.ScopedGetOrAddAsync("42", _ => Task.FromResult(new Scoped<Disposable>(new Disposable(42))));
var alternate = cache.GetAsyncAlternateLookup<ReadOnlySpan<char>>();
ReadOnlySpan<char> key = "42";

alternate.TryRemove(key, out var actualKey).Should().BeTrue();

actualKey.Should().Be("42");
cache.ScopedTryGet("42", out _).Should().BeFalse();
}

[Fact]
public async Task AltScopedGetOrAddAsyncRetrievesCachedValue()
{
var cache = new AtomicFactoryScopedAsyncCache<string, Disposable>(new ConcurrentLru<string, ScopedAsyncAtomicFactory<string, Disposable>>(1, capacity, StringComparer.Ordinal));
var alternate = cache.GetAsyncAlternateLookup<ReadOnlySpan<char>>();
var factoryCalls = 0;
var key = "42";

using var lifetime1 = await alternate.ScopedGetOrAddAsync(key.AsSpan(), k =>
{
factoryCalls++;
return Task.FromResult(new Scoped<Disposable>(new Disposable(int.Parse(k))));
});

using var lifetime2 = await alternate.ScopedGetOrAddAsync(key.AsSpan(), k =>
{
factoryCalls++;
return Task.FromResult(new Scoped<Disposable>(new Disposable(123)));
});

lifetime1.Value.State.Should().Be(42);
lifetime2.Value.State.Should().Be(42);
factoryCalls.Should().Be(1);
}

[Fact]
public async Task AltScopedGetOrAddArgAsyncRetrievesCachedValue()
{
var cache = new AtomicFactoryScopedAsyncCache<string, Disposable>(new ConcurrentLru<string, ScopedAsyncAtomicFactory<string, Disposable>>(1, capacity, StringComparer.Ordinal));
var alternate = cache.GetAsyncAlternateLookup<ReadOnlySpan<char>>();
var factoryCalls = 0;
var key = "42";

using var lifetime1 = await alternate.ScopedGetOrAddAsync(key.AsSpan(), (k, offset) =>
{
factoryCalls++;
return Task.FromResult(new Scoped<Disposable>(new Disposable(int.Parse(k) + offset)));
}, 1);

using var lifetime2 = await alternate.ScopedGetOrAddAsync(key.AsSpan(), (k, offset) =>
{
factoryCalls++;
return Task.FromResult(new Scoped<Disposable>(new Disposable(int.Parse(k) + offset)));
}, 2);

lifetime1.Value.State.Should().Be(43);
lifetime2.Value.State.Should().Be(43);
factoryCalls.Should().Be(1);
}

[Fact]
public async Task AltScopedTryGetDisposedScopeReturnsFalse()
{
var cache = new AtomicFactoryScopedAsyncCache<string, Disposable>(new ConcurrentLru<string, ScopedAsyncAtomicFactory<string, Disposable>>(capacity));
var alternate = cache.GetAsyncAlternateLookup<ReadOnlySpan<char>>();
var scope = new Scoped<Disposable>(new Disposable());

await cache.ScopedGetOrAddAsync("a", _ => Task.FromResult(scope));

scope.Dispose();

alternate.ScopedTryGet("a", out var lifetime).Should().BeFalse();
}

[Fact]
public void WhenItemDoesNotExistTryGetAltReturnsFalse()
{
var cache = new AtomicFactoryScopedAsyncCache<string, Disposable>(new ConcurrentLru<string, ScopedAsyncAtomicFactory<string, Disposable>>(capacity));
var alternate = cache.GetAsyncAlternateLookup<ReadOnlySpan<char>>();
alternate.ScopedTryGet("a", out _).Should().BeFalse();
}

[Fact]
public void WhenKeyExistsTryRemoveAltReturnsTrue()
{
var cache = new AtomicFactoryScopedAsyncCache<string, Disposable>(new ConcurrentLru<string, ScopedAsyncAtomicFactory<string, Disposable>>(capacity));
var alternate = cache.GetAsyncAlternateLookup<ReadOnlySpan<char>>();

cache.AddOrUpdate("a", new Disposable());
alternate.TryRemove("a", out var key).Should().BeTrue();
key.Should().Be("a");
}

[Fact]
public void WhenKeyDoesNotExistTryRemoveAltReturnsFalse()
{
var cache = new AtomicFactoryScopedAsyncCache<string, Disposable>(new ConcurrentLru<string, ScopedAsyncAtomicFactory<string, Disposable>>(capacity));
var alternate = cache.GetAsyncAlternateLookup<ReadOnlySpan<char>>();
alternate.TryRemove("a", out _).Should().BeFalse();
}

[Fact]
public async Task GetOrAddAltDisposedScopeThrows()
{
var cache = new AtomicFactoryScopedAsyncCache<string, Disposable>(new ConcurrentLru<string, ScopedAsyncAtomicFactory<string, Disposable>>(capacity));
var alternate = cache.GetAsyncAlternateLookup<ReadOnlySpan<char>>();

var scope = new Scoped<Disposable>(new Disposable());
scope.Dispose();

Func<Task> getOrAdd = async () => { await this.cache.ScopedGetOrAddAsync(1, k => Task.FromResult(scope)); };

await getOrAdd.Should().ThrowAsync<InvalidOperationException>();
}
#endif

[Fact]
Expand Down Expand Up @@ -94,5 +238,77 @@ public void WhenEntryIsUpdatedOldEntryIsDisposed()
this.cache.TryUpdate(1, new Disposable()).Should().BeTrue();
disposable2.IsDisposed.Should().BeTrue();
}

#if NET9_0_OR_GREATER

[Fact]
public async Task ScopedGetOrAddAsyncDisposedScopeThrowsInvalidOperationException()
{
var cache = new AtomicFactoryScopedAsyncCache<string, Disposable>(new ConcurrentLru<string, ScopedAsyncAtomicFactory<string, Disposable>>(1, capacity, StringComparer.Ordinal));
var alternate = cache.GetAsyncAlternateLookup<ReadOnlySpan<char>>();

var scope = new Scoped<Disposable>(new Disposable());
scope.Dispose();

Func<Task> getOrAdd = async () => { await alternate.ScopedGetOrAddAsync("a", _ => Task.FromResult(scope)); };

await getOrAdd.Should().ThrowAsync<InvalidOperationException>();
}

[Fact]
public void AltTryUpdateMissingKeyReturnsFalse()
{
var cache = new AtomicFactoryScopedAsyncCache<string, Disposable>(new ConcurrentLru<string, ScopedAsyncAtomicFactory<string, Disposable>>(1, capacity, StringComparer.Ordinal));
var alternate = cache.GetAsyncAlternateLookup<ReadOnlySpan<char>>();
var key = "42";

alternate.TryUpdate(key.AsSpan(), new Disposable(42)).Should().BeFalse();
cache.ScopedTryGet("42", out _).Should().BeFalse();
}

[Fact]
public async Task AltTryUpdateExistingKeyUpdatesValue()
{
var cache = new AtomicFactoryScopedAsyncCache<string, Disposable>(new ConcurrentLru<string, ScopedAsyncAtomicFactory<string, Disposable>>(1, capacity, StringComparer.Ordinal));
var alternate = cache.GetAsyncAlternateLookup<ReadOnlySpan<char>>();
var key = "42";

using var lifetime = await cache.ScopedGetOrAddAsync("42", _ => Task.FromResult(new Scoped<Disposable>(new Disposable(1))));
lifetime.Dispose();

alternate.TryUpdate(key.AsSpan(), new Disposable(2)).Should().BeTrue();

alternate.ScopedTryGet(key.AsSpan(), out var updatedLifetime).Should().BeTrue();
updatedLifetime.Value.State.Should().Be(2);
updatedLifetime.Dispose();
}

[Fact]
public void AltAddOrUpdateMissingKeyAddsValue()
{
var cache = new AtomicFactoryScopedAsyncCache<string, Disposable>(new ConcurrentLru<string, ScopedAsyncAtomicFactory<string, Disposable>>(1, capacity, StringComparer.Ordinal));
var alternate = cache.GetAsyncAlternateLookup<ReadOnlySpan<char>>();
ReadOnlySpan<char> key = "42";

alternate.AddOrUpdate(key, new Disposable(42));
alternate.ScopedTryGet(key, out var lifetime).Should().BeTrue();
lifetime.Value.State.Should().Be(42);
lifetime.Dispose();
}

[Fact]
public void AltAddOrUpdateExistingKeyUpdatesValue()
{
var cache = new AtomicFactoryScopedAsyncCache<string, Disposable>(new ConcurrentLru<string, ScopedAsyncAtomicFactory<string, Disposable>>(1, capacity, StringComparer.Ordinal));
var alternate = cache.GetAsyncAlternateLookup<ReadOnlySpan<char>>();
ReadOnlySpan<char> key = "42";

alternate.AddOrUpdate(key, new Disposable(42));
alternate.AddOrUpdate(key, new Disposable(43));
alternate.ScopedTryGet(key, out var updatedLifetime).Should().BeTrue();
updatedLifetime.Value.State.Should().Be(43);
updatedLifetime.Dispose();
}
#endif
}
}
Loading
Loading