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,162 @@
#if NET9_0_OR_GREATER
using System;
using System.Threading.Tasks;
using BitFaster.Caching.Atomic;
using BitFaster.Caching.Lru;
using FluentAssertions;
using Xunit;

namespace BitFaster.Caching.UnitTests.Atomic
{
public class AtomicFactoryAsyncCacheAsyncAlternateLookupTests
{
private readonly AtomicFactoryAsyncCache<string, string> cache;

public AtomicFactoryAsyncCacheAsyncAlternateLookupTests()
{
var innerCache = new ConcurrentLru<string, AsyncAtomicFactory<string, string>>(1, 9, StringComparer.Ordinal);
cache = new AtomicFactoryAsyncCache<string, string>(innerCache);
}

[Fact]
public void TryGetAsyncAlternateLookupReturnsLookupForCompatibleComparer()
{
cache.AddOrUpdate("42", "value");
ReadOnlySpan<char> key = "42";

cache.TryGetAsyncAlternateLookup<ReadOnlySpan<char>>(out var alternate).Should().BeTrue();
alternate.TryGet(key, out var value).Should().BeTrue();
value.Should().Be("value");
}

[Fact]
public void GetAsyncAlternateLookupThrowsForIncompatibleComparer()
{
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 void AsyncAlternateLookupTryGetReturnsFalseForMissingKey()
{
var alternate = cache.GetAsyncAlternateLookup<ReadOnlySpan<char>>();
ReadOnlySpan<char> key = "42";

alternate.TryGet(key, out _).Should().BeFalse();
}

[Fact]
public void AsyncAlternateLookupTryRemoveReturnsActualKeyAndValue()
{
cache.AddOrUpdate("42", "value");
var alternate = cache.GetAsyncAlternateLookup<ReadOnlySpan<char>>();
ReadOnlySpan<char> key = "42";

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

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

[Fact]
public void AsyncAlternateLookupTryRemoveReturnsFalseForMissingKey()
{
var alternate = cache.GetAsyncAlternateLookup<ReadOnlySpan<char>>();
ReadOnlySpan<char> key = "42";

alternate.TryRemove(key, out _, out _).Should().BeFalse();
}

[Fact]
public void AsyncAlternateLookupTryUpdateReturnsFalseForMissingKeyAndUpdatesExistingValue()
{
var alternate = cache.GetAsyncAlternateLookup<ReadOnlySpan<char>>();
ReadOnlySpan<char> key = "42";

alternate.TryUpdate(key, "value-42").Should().BeFalse();
cache.TryGet("42", out _).Should().BeFalse();

cache.AddOrUpdate("42", "value-42");
alternate.TryUpdate(key, "updated").Should().BeTrue();

cache.TryGet("42", out var value).Should().BeTrue();
value.Should().Be("updated");
alternate.TryGet(key, out value).Should().BeTrue();
value.Should().Be("updated");
}

[Fact]
public void AsyncAlternateLookupAddOrUpdateAddsMissingValueAndUpdatesExistingValue()
{
var alternate = cache.GetAsyncAlternateLookup<ReadOnlySpan<char>>();
ReadOnlySpan<char> key = "42";

alternate.AddOrUpdate(key, "value-42");

cache.TryGet("42", out var value).Should().BeTrue();
value.Should().Be("value-42");

alternate.AddOrUpdate(key, "updated");

cache.TryGet("42", out value).Should().BeTrue();
value.Should().Be("updated");
alternate.TryGet(key, out value).Should().BeTrue();
value.Should().Be("updated");
}

[Fact]
public async Task AsyncAlternateLookupGetOrAddAsyncUsesActualKeyOnMissAndHit()
{
var alternate = cache.GetAsyncAlternateLookup<ReadOnlySpan<char>>();
var factoryCalls = 0;

var result = await alternate.GetOrAddAsync("42".AsSpan(), key =>
{
factoryCalls++;
return Task.FromResult($"value-{key}");
});
result.Should().Be("value-42");

result = await alternate.GetOrAddAsync("42".AsSpan(), key =>
{
factoryCalls++;
return Task.FromResult("unused");
});
result.Should().Be("value-42");

factoryCalls.Should().Be(1);
cache.TryGet("42", out var value).Should().BeTrue();
value.Should().Be("value-42");
}

[Fact]
public async Task AsyncAlternateLookupGetOrAddAsyncWithArgUsesActualKeyOnMissAndHit()
{
var alternate = cache.GetAsyncAlternateLookup<ReadOnlySpan<char>>();
var factoryCalls = 0;

var result = await alternate.GetOrAddAsync("42".AsSpan(), (key, prefix) =>
{
factoryCalls++;
return Task.FromResult($"{prefix}{key}");
}, "value-");
result.Should().Be("value-42");

result = await alternate.GetOrAddAsync("42".AsSpan(), (key, prefix) =>
{
factoryCalls++;
return Task.FromResult("unused");
}, "unused-");
result.Should().Be("value-42");

factoryCalls.Should().Be(1);
cache.TryGet("42", out var value).Should().BeTrue();
value.Should().Be("value-42");
}
}
}
#endif
99 changes: 99 additions & 0 deletions BitFaster.Caching/Atomic/AtomicFactoryAsyncCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,105 @@ public bool TryUpdate(K key, V value)
return cache.TryUpdate(key, new AsyncAtomicFactory<K, V>(value));
}

#if NET9_0_OR_GREATER
///<inheritdoc/>
public IAsyncAlternateLookup<TAlternateKey, K, V> GetAsyncAlternateLookup<TAlternateKey>()
Comment thread
bitfaster marked this conversation as resolved.
where TAlternateKey : notnull, allows ref struct
{
var inner = cache.GetAlternateLookup<TAlternateKey>();
var comparer = (IAlternateEqualityComparer<TAlternateKey, K>)cache.Comparer;
return new AlternateLookup<TAlternateKey>(inner, comparer);
}

///<inheritdoc/>
public bool TryGetAsyncAlternateLookup<TAlternateKey>([MaybeNullWhen(false)] out IAsyncAlternateLookup<TAlternateKey, K, V> lookup)
where TAlternateKey : notnull, allows ref struct
{
if (cache.TryGetAlternateLookup<TAlternateKey>(out var inner))
{
var comparer = (IAlternateEqualityComparer<TAlternateKey, K>)cache.Comparer;
lookup = new AlternateLookup<TAlternateKey>(inner, comparer);
return true;
}

lookup = default;
return false;
}

internal readonly struct AlternateLookup<TAlternateKey> : IAsyncAlternateLookup<TAlternateKey, K, V>
where TAlternateKey : notnull, allows ref struct
{
private readonly IAlternateLookup<TAlternateKey, K, AsyncAtomicFactory<K, V>> inner;
private readonly IAlternateEqualityComparer<TAlternateKey, K> comparer;

internal AlternateLookup(IAlternateLookup<TAlternateKey, K, AsyncAtomicFactory<K, V>> inner, IAlternateEqualityComparer<TAlternateKey, K> comparer)
{
this.inner = inner;
this.comparer = comparer;
}

public bool TryGet(TAlternateKey key, [MaybeNullWhen(false)] out V value)
{
if (inner.TryGet(key, out var atomic) && atomic.IsValueCreated)
{
value = atomic.ValueIfCreated!;
return true;
}

value = default;
return false;
}

public bool TryRemove(TAlternateKey key, [MaybeNullWhen(false)] out K actualKey, [MaybeNullWhen(false)] out V value)
{
if (inner.TryRemove(key, out actualKey, out var atomic))
{
value = atomic.ValueIfCreated!;
return true;
}

value = default;
return false;
}

public bool TryUpdate(TAlternateKey key, V value)
{
return inner.TryUpdate(key, new AsyncAtomicFactory<K, V>(value));
}

public void AddOrUpdate(TAlternateKey key, V value)
{
inner.AddOrUpdate(key, new AsyncAtomicFactory<K, V>(value));
}

public ValueTask<V> GetOrAddAsync(TAlternateKey key, Func<K, Task<V>> valueFactory)
{
var synchronized = inner.GetOrAdd(key, _ => new AsyncAtomicFactory<K, V>());

if (synchronized.IsValueCreated)
{
return new ValueTask<V>(synchronized.ValueIfCreated!);
}

K actualKey = comparer.Create(key);
return synchronized.GetValueAsync(actualKey, valueFactory);
}

public ValueTask<V> GetOrAddAsync<TArg>(TAlternateKey key, Func<K, TArg, Task<V>> valueFactory, TArg factoryArgument)
{
var synchronized = inner.GetOrAdd(key, _ => new AsyncAtomicFactory<K, V>());

if (synchronized.IsValueCreated)
{
return new ValueTask<V>(synchronized.ValueIfCreated!);
}

K actualKey = comparer.Create(key);
return synchronized.GetValueAsync(actualKey, valueFactory, factoryArgument);
}
}
#endif

///<inheritdoc/>
public IEnumerator<KeyValuePair<K, V>> GetEnumerator()
{
Expand Down
25 changes: 25 additions & 0 deletions BitFaster.Caching/IAsyncCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -118,5 +118,30 @@ public interface IAsyncCache<K, V> : IEnumerable<KeyValuePair<K, V>>
/// Removes all keys and values from the cache.
/// </summary>
void Clear();

#if NET9_0_OR_GREATER
// backcompat: add not null constraint to IAsyncCache (where K : notnull)
#pragma warning disable CS8714 // The type cannot be used as type parameter in the generic type or method. Nullability of type argument doesn't match 'notnull' constraint.
/// <summary>
/// Gets an async alternate lookup that can use an alternate key type with the configured comparer.
/// </summary>
/// <typeparam name="TAlternateKey">The alternate key type.</typeparam>
/// <returns>An async alternate lookup.</returns>
/// <exception cref="InvalidOperationException">The configured comparer does not support <typeparamref name="TAlternateKey" />.</exception>
IAsyncAlternateLookup<TAlternateKey, K, V> GetAsyncAlternateLookup<TAlternateKey>()
where TAlternateKey : notnull, allows ref struct
=> throw new NotSupportedException();

/// <summary>
/// Attempts to get an async alternate lookup that can use an alternate key type with the configured comparer.
/// </summary>
/// <typeparam name="TAlternateKey">The alternate key type.</typeparam>
/// <param name="lookup">The async alternate lookup when available.</param>
/// <returns><see langword="true" /> when the configured comparer supports <typeparamref name="TAlternateKey" />; otherwise, <see langword="false" />.</returns>
bool TryGetAsyncAlternateLookup<TAlternateKey>([MaybeNullWhen(false)] out IAsyncAlternateLookup<TAlternateKey, K, V> lookup)
where TAlternateKey : notnull, allows ref struct
=> throw new NotSupportedException();
#pragma warning restore CS8714
#endif
}
}
14 changes: 2 additions & 12 deletions BitFaster.Caching/Lfu/ConcurrentLfu.cs
Original file line number Diff line number Diff line change
Expand Up @@ -219,24 +219,14 @@ public bool TryGetAlternateLookup<TAlternateKey>([MaybeNullWhen(false)] out IAlt
return core.TryGetAlternateLookup(out lookup);
}

/// <summary>
/// Gets an async alternate lookup that can use an alternate key type with the configured comparer.
/// </summary>
/// <typeparam name="TAlternateKey">The alternate key type.</typeparam>
/// <returns>An async alternate lookup.</returns>
/// <exception cref="InvalidOperationException">The configured comparer does not support <typeparamref name="TAlternateKey" />.</exception>
///<inheritdoc/>
public IAsyncAlternateLookup<TAlternateKey, K, V> GetAsyncAlternateLookup<TAlternateKey>()
where TAlternateKey : notnull, allows ref struct
{
return core.GetAsyncAlternateLookup<TAlternateKey>();
}

/// <summary>
/// Attempts to get an async alternate lookup that can use an alternate key type with the configured comparer.
/// </summary>
/// <typeparam name="TAlternateKey">The alternate key type.</typeparam>
/// <param name="lookup">The async alternate lookup when available.</param>
/// <returns><see langword="true" /> when the configured comparer supports <typeparamref name="TAlternateKey" />; otherwise, <see langword="false" />.</returns>
///<inheritdoc/>
public bool TryGetAsyncAlternateLookup<TAlternateKey>([MaybeNullWhen(false)] out IAsyncAlternateLookup<TAlternateKey, K, V> lookup)
where TAlternateKey : notnull, allows ref struct
{
Expand Down
14 changes: 2 additions & 12 deletions BitFaster.Caching/Lfu/ConcurrentLfuCore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -999,12 +999,7 @@ public bool TryGetAlternateLookup<TAlternateKey>([MaybeNullWhen(false)] out IAlt
return false;
}

/// <summary>
/// Gets an async alternate lookup that can use an alternate key type with the configured comparer.
/// </summary>
/// <typeparam name="TAlternateKey">The alternate key type.</typeparam>
/// <returns>An async alternate lookup.</returns>
/// <exception cref="InvalidOperationException">The configured comparer does not support <typeparamref name="TAlternateKey" />.</exception>
///<inheritdoc/>
public IAsyncAlternateLookup<TAlternateKey, K, V> GetAsyncAlternateLookup<TAlternateKey>()
where TAlternateKey : notnull, allows ref struct
{
Expand All @@ -1016,12 +1011,7 @@ public IAsyncAlternateLookup<TAlternateKey, K, V> GetAsyncAlternateLookup<TAlter
return new AlternateLookup<TAlternateKey>(this);
}

/// <summary>
/// Attempts to get an async alternate lookup that can use an alternate key type with the configured comparer.
/// </summary>
/// <typeparam name="TAlternateKey">The alternate key type.</typeparam>
/// <param name="lookup">The async alternate lookup when available.</param>
/// <returns><see langword="true" /> when the configured comparer supports <typeparamref name="TAlternateKey" />; otherwise, <see langword="false" />.</returns>
///<inheritdoc/>
public bool TryGetAsyncAlternateLookup<TAlternateKey>([MaybeNullWhen(false)] out IAsyncAlternateLookup<TAlternateKey, K, V> lookup)
where TAlternateKey : notnull, allows ref struct
{
Expand Down
14 changes: 2 additions & 12 deletions BitFaster.Caching/Lfu/ConcurrentTLfu.cs
Original file line number Diff line number Diff line change
Expand Up @@ -159,24 +159,14 @@ public bool TryGetAlternateLookup<TAlternateKey>([MaybeNullWhen(false)] out IAlt
return core.TryGetAlternateLookup(out lookup);
}

/// <summary>
/// Gets an async alternate lookup that can use an alternate key type with the configured comparer.
/// </summary>
/// <typeparam name="TAlternateKey">The alternate key type.</typeparam>
/// <returns>An async alternate lookup.</returns>
/// <exception cref="InvalidOperationException">The configured comparer does not support <typeparamref name="TAlternateKey" />.</exception>
///<inheritdoc/>
public IAsyncAlternateLookup<TAlternateKey, K, V> GetAsyncAlternateLookup<TAlternateKey>()
where TAlternateKey : notnull, allows ref struct
{
return core.GetAsyncAlternateLookup<TAlternateKey>();
}

/// <summary>
/// Attempts to get an async alternate lookup that can use an alternate key type with the configured comparer.
/// </summary>
/// <typeparam name="TAlternateKey">The alternate key type.</typeparam>
/// <param name="lookup">The async alternate lookup when available.</param>
/// <returns><see langword="true" /> when the configured comparer supports <typeparamref name="TAlternateKey" />; otherwise, <see langword="false" />.</returns>
///<inheritdoc/>
public bool TryGetAsyncAlternateLookup<TAlternateKey>([MaybeNullWhen(false)] out IAsyncAlternateLookup<TAlternateKey, K, V> lookup)
where TAlternateKey : notnull, allows ref struct
{
Expand Down
Loading
Loading