diff --git a/CHANGELOG.md b/CHANGELOG.md
index 82c160aec2..20b721ef8a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,29 @@
Release Notes
====
+# 08-13-2024
+DotNext 5.12.0
+* Added `DotNext.Runtime.ValueReference` data type that allows to obtain async-friendly managed pointer to the field
+* Deprecation of `ConcurrentCache` in favor of `RandomAccessCache`
+
+DotNext.Metaprogramming 5.12.0
+* Updated dependencies
+
+DotNext.Unsafe 5.12.0
+* Updated dependencies
+
+DotNext.Threading 5.12.0
+* Introduced async-friendly `RandomAccessCache` as a replacement for deprecated `ConcurrentCache`. It uses [SIEVE](https://cachemon.github.io/SIEVE-website/) eviction algorithm.
+
+DotNext.IO 5.12.0
+* Updated dependencies
+
+DotNext.Net.Cluster 5.12.0
+* Fixed cancellation of `PersistentState` async methods
+
+DotNext.AspNetCore.Cluster 5.12.0
+* Fixed cancellation of `PersistentState` async methods
+
# 08-01-2024
DotNext 5.11.0
* Added `DotNext.Threading.Epoch` for epoch-based reclamation
diff --git a/README.md b/README.md
index 5c22663ba5..e3c9342a4e 100644
--- a/README.md
+++ b/README.md
@@ -44,35 +44,29 @@ All these things are implemented in 100% managed code on top of existing .NET AP
* [NuGet Packages](https://www.nuget.org/profiles/rvsakno)
# What's new
-Release Date: 08-01-2024
+Release Date: 08-14-2024
-DotNext 5.11.0
-* Added `DotNext.Threading.Epoch` for epoch-based reclamation
-* Fixed one-shot FNV1a hashing method
-* Fixed [248](https://github.com/dotnet/dotNext/issues/248)
-* Minor performance improvements
+DotNext 5.12.0
+* Added `DotNext.Runtime.ValueReference` data type that allows to obtain async-friendly managed pointer to the field
+* Deprecation of `ConcurrentCache` in favor of `RandomAccessCache`
-DotNext.Metaprogramming 5.11.0
-* Minor performance improvements
+DotNext.Metaprogramming 5.12.0
* Updated dependencies
-DotNext.Unsafe 5.11.0
-* Minor performance improvements
+DotNext.Unsafe 5.12.0
* Updated dependencies
-DotNext.Threading 5.11.0
-* Fixed `AsyncSharedLock.Downgrade` behavior, so it can be used to release a weak lock
-* Updated dependencies
+DotNext.Threading 5.12.0
+* Introduced async-friendly `RandomAccessCache` as a replacement for deprecated `ConcurrentCache`. It uses [SIEVE](https://cachemon.github.io/SIEVE-website/) eviction algorithm.
-DotNext.IO 5.11.0
-* Minor performance improvements
+DotNext.IO 5.12.0
* Updated dependencies
-DotNext.Net.Cluster 5.11.0
-* Updated dependencies
+DotNext.Net.Cluster 5.12.0
+* Fixed cancellation of `PersistentState` async methods
-DotNext.AspNetCore.Cluster 5.11.0
-* Updated dependencies
+DotNext.AspNetCore.Cluster 5.12.0
+* Fixed cancellation of `PersistentState` async methods
Changelog for previous versions located [here](./CHANGELOG.md).
diff --git a/src/DotNext.Benchmarks/Runtime/Caching/ConcurrentCacheBenchmark.cs b/src/DotNext.Benchmarks/Runtime/Caching/ConcurrentCacheBenchmark.cs
deleted file mode 100644
index 6cc122cab7..0000000000
--- a/src/DotNext.Benchmarks/Runtime/Caching/ConcurrentCacheBenchmark.cs
+++ /dev/null
@@ -1,119 +0,0 @@
-using System;
-using System.Collections.Concurrent;
-using System.Threading;
-using BenchmarkDotNet.Attributes;
-using BenchmarkDotNet.Engines;
-using BenchmarkDotNet.Order;
-
-namespace DotNext.Runtime.Caching;
-
-[SimpleJob(runStrategy: RunStrategy.Throughput, launchCount: 1)]
-[Orderer(SummaryOrderPolicy.Declared)]
-[MemoryDiagnoser]
-public class ConcurrentCacheBenchmark
-{
- private const int Capacity = 10;
-
- private Thread[] threads;
-
- [Params(2, 3, 4, 5)]
- public int threadCount;
-
- [Params(CacheEvictionPolicy.LRU, CacheEvictionPolicy.LFU)]
- public CacheEvictionPolicy evictionPolicy;
-
- private ConcurrentCache cache;
- private ConcurrentDictionary dictionary;
-
- [IterationSetup(Target = nameof(ConcurrentCacheRead))]
- public void InitializeConcurrentCacheAccess()
- {
- cache = new(Capacity, Environment.ProcessorCount, evictionPolicy);
-
- // fill cache
- for (var i = 0; i < Capacity; i++)
- cache[i] = i.ToString();
-
- // read from cache
- threads = new Thread[threadCount];
-
- foreach (ref var thread in threads.AsSpan())
- thread = new Thread(Run);
-
- void Run()
- {
- var rnd = new Random();
-
- for (var i = 0; i < 100; i++)
- TouchCache(rnd);
- }
-
- void TouchCache(Random random)
- {
- var index = random.Next(Capacity);
- cache.TryGetValue(index, out _);
- }
- }
-
- [IterationCleanup(Target = nameof(ConcurrentCacheRead))]
- public void CleanupCache()
- {
- cache.Clear();
- }
-
- [IterationSetup(Target = nameof(ConcurrentDictionaryRead))]
- public void InitializeConcurrentDictionaryAccess()
- {
- dictionary = new(Environment.ProcessorCount, Capacity);
-
- // fill cache
- for (var i = 0; i < Capacity; i++)
- dictionary[i] = i.ToString();
-
- // read from cache
- threads = new Thread[threadCount];
-
- foreach (ref var thread in threads.AsSpan())
- thread = new Thread(Run);
-
- void Run()
- {
- var rnd = new Random();
-
- for (var i = 0; i < 100; i++)
- TouchDictionary(rnd);
- }
-
- void TouchDictionary(Random random)
- {
- var index = random.Next(Capacity);
- dictionary.TryGetValue(index, out _);
- }
- }
-
- [IterationCleanup(Target = nameof(ConcurrentDictionaryRead))]
- public void CleanupDictionary()
- {
- dictionary.Clear();
- }
-
- [Benchmark(Baseline = true)]
- public void ConcurrentDictionaryRead()
- {
- foreach (var thread in threads)
- thread.Start();
-
- foreach (var thread in threads)
- thread.Join();
- }
-
- [Benchmark]
- public void ConcurrentCacheRead()
- {
- foreach (var thread in threads)
- thread.Start();
-
- foreach (var thread in threads)
- thread.Join();
- }
-}
\ No newline at end of file
diff --git a/src/DotNext.IO/DotNext.IO.csproj b/src/DotNext.IO/DotNext.IO.csproj
index bad3c8e545..26f283b247 100644
--- a/src/DotNext.IO/DotNext.IO.csproj
+++ b/src/DotNext.IO/DotNext.IO.csproj
@@ -11,7 +11,7 @@
.NET Foundation and Contributors
.NEXT Family of Libraries
- 5.11.0
+ 5.12.0
DotNext.IO
MIT
diff --git a/src/DotNext.Metaprogramming/DotNext.Metaprogramming.csproj b/src/DotNext.Metaprogramming/DotNext.Metaprogramming.csproj
index 45c8f7b035..30c63b4c8d 100644
--- a/src/DotNext.Metaprogramming/DotNext.Metaprogramming.csproj
+++ b/src/DotNext.Metaprogramming/DotNext.Metaprogramming.csproj
@@ -8,7 +8,7 @@
true
false
nullablePublicOnly
- 5.11.0
+ 5.12.0
.NET Foundation
.NEXT Family of Libraries
diff --git a/src/DotNext.Tests/DotNext.Tests.csproj b/src/DotNext.Tests/DotNext.Tests.csproj
index efdd6cffae..c1d8c4265c 100644
--- a/src/DotNext.Tests/DotNext.Tests.csproj
+++ b/src/DotNext.Tests/DotNext.Tests.csproj
@@ -56,4 +56,8 @@
+
+
+
+
diff --git a/src/DotNext.Tests/Runtime/Caching/ConcurrentCacheTests.cs b/src/DotNext.Tests/Runtime/Caching/ConcurrentCacheTests.cs
index 261a8b3fba..ea0d31f37f 100644
--- a/src/DotNext.Tests/Runtime/Caching/ConcurrentCacheTests.cs
+++ b/src/DotNext.Tests/Runtime/Caching/ConcurrentCacheTests.cs
@@ -1,5 +1,6 @@
namespace DotNext.Runtime.Caching;
+[Obsolete]
public sealed class ConcurrentCacheTests : Test
{
[Fact]
diff --git a/src/DotNext.Tests/Runtime/Caching/RandomAccessCacheTests.cs b/src/DotNext.Tests/Runtime/Caching/RandomAccessCacheTests.cs
new file mode 100644
index 0000000000..8e496332e7
--- /dev/null
+++ b/src/DotNext.Tests/Runtime/Caching/RandomAccessCacheTests.cs
@@ -0,0 +1,195 @@
+using System.Runtime.CompilerServices;
+
+namespace DotNext.Runtime.Caching;
+
+using CompilerServices;
+
+public sealed class RandomAccessCacheTests : Test
+{
+ [Fact]
+ public static async Task CacheOverflow()
+ {
+ var evictedItem = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
+ await using var cache = new RandomAccessCache(15)
+ {
+ Eviction = (_, value) => evictedItem.TrySetResult(value),
+ };
+
+ for (long i = 0; i < 150; i++)
+ {
+ using var handle = await cache.ChangeAsync(i);
+ False(handle.TryGetValue(out _));
+
+ handle.SetValue(i.ToString());
+ }
+
+ Equal("0", await evictedItem.Task.WaitAsync(DefaultTimeout));
+ }
+
+ [Fact]
+ public static async Task CacheOverflow2()
+ {
+ var evictedItem = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
+ await using var cache = new RandomAccessCache(15)
+ {
+ Eviction = (_, value) => evictedItem.TrySetResult(value),
+ };
+
+ const long accessCount = 150;
+ for (long i = 0; i < accessCount; i++)
+ {
+ var key = Random.Shared.NextInt64(accessCount);
+ if (cache.TryRead(key, out var readSession))
+ {
+ using (readSession)
+ {
+ Equal(key.ToString(), readSession.Value);
+ }
+ }
+ else
+ {
+ using var writeSession = await cache.ChangeAsync(key);
+ if (writeSession.TryGetValue(out var value))
+ {
+ Equal(key.ToString(), value);
+ }
+ else
+ {
+ writeSession.SetValue(key.ToString());
+ }
+ }
+ }
+
+ await evictedItem.Task;
+ }
+
+ [Fact]
+ public static async Task StressTest()
+ {
+ await using var cache = new RandomAccessCache(15);
+
+ const long accessCount = 1500;
+
+ var task1 = RequestLoop(cache);
+ var task2 = RequestLoop(cache);
+
+ await Task.WhenAll(task1, task2);
+
+ [AsyncMethodBuilder(typeof(SpawningAsyncTaskMethodBuilder))]
+ static async Task RequestLoop(RandomAccessCache cache)
+ {
+ for (long i = 0; i < accessCount; i++)
+ {
+ var key = Random.Shared.NextInt64(accessCount);
+ if (cache.TryRead(key, out var readSession))
+ {
+ using (readSession)
+ {
+ Equal(key.ToString(), readSession.Value);
+ }
+ }
+ else
+ {
+ using var writeSession = await cache.ChangeAsync(key);
+ if (writeSession.TryGetValue(out var value))
+ {
+ Equal(key.ToString(), value);
+ }
+ else
+ {
+ writeSession.SetValue(key.ToString());
+ }
+ }
+ }
+ }
+ }
+
+ [Fact]
+ public static async Task AddRemove()
+ {
+ await using var cache = new RandomAccessCache(15);
+
+ using (var session = await cache.ChangeAsync(10L))
+ {
+ False(session.TryGetValue(out _));
+ session.SetValue("10");
+ }
+
+ Null(await cache.TryRemoveAsync(11L));
+
+ using (var session = (await cache.TryRemoveAsync(10L)).Value)
+ {
+ Equal("10", session.Value);
+ }
+ }
+
+ [Fact]
+ public static async Task AddInvalidate()
+ {
+ await using var cache = new RandomAccessCache(15);
+
+ using (var session = await cache.ChangeAsync(10L))
+ {
+ False(session.TryGetValue(out _));
+ session.SetValue("10");
+ }
+
+ False(await cache.InvalidateAsync(11L));
+ True(await cache.InvalidateAsync(10L));
+ }
+
+ [Fact]
+ public static async Task AddTwice()
+ {
+ await using var cache = new RandomAccessCache(15);
+
+ using (var session = await cache.ChangeAsync(10L))
+ {
+ False(session.TryGetValue(out _));
+ session.SetValue("10");
+
+ Throws(() => session.SetValue("20"));
+ }
+ }
+
+ [Fact]
+ public static async Task DisposedCacheAccess()
+ {
+ var cache = new RandomAccessCache(18);
+ await cache.DisposeAsync();
+
+ await ThrowsAsync(cache.ChangeAsync(0L).AsTask);
+ await ThrowsAsync(cache.TryRemoveAsync(0L).AsTask);
+ await ThrowsAsync(cache.InvalidateAsync().AsTask);
+ await ThrowsAsync(cache.InvalidateAsync(10L).AsTask);
+ }
+
+ [Fact]
+ public static async Task DisposedCacheAccess2()
+ {
+ using var cts = new CancellationTokenSource();
+ var cache = new RandomAccessCache(18);
+ await cache.DisposeAsync();
+
+ await ThrowsAsync(cache.ChangeAsync(0L, cts.Token).AsTask);
+ await ThrowsAsync(cache.TryRemoveAsync(0L, cts.Token).AsTask);
+ await ThrowsAsync(cache.InvalidateAsync(cts.Token).AsTask);
+ await ThrowsAsync(cache.InvalidateAsync(10L, cts.Token).AsTask);
+ }
+
+ [Fact]
+ public static async Task Invalidation()
+ {
+ await using var cache = new RandomAccessCache(15);
+
+ for (long i = 0; i < 20; i++)
+ {
+ using var handle = await cache.ChangeAsync(i);
+ False(handle.TryGetValue(out _));
+
+ handle.SetValue(i.ToString());
+ }
+
+ await cache.InvalidateAsync();
+ }
+}
\ No newline at end of file
diff --git a/src/DotNext.Tests/Runtime/ValueReferenceTests.cs b/src/DotNext.Tests/Runtime/ValueReferenceTests.cs
new file mode 100644
index 0000000000..0016f733f2
--- /dev/null
+++ b/src/DotNext.Tests/Runtime/ValueReferenceTests.cs
@@ -0,0 +1,107 @@
+using System.Runtime.CompilerServices;
+
+namespace DotNext.Runtime;
+
+public sealed class ValueReferenceTests : Test
+{
+ [Fact]
+ public static void MutableFieldRef()
+ {
+ var obj = new MyClass() { AnotherField = string.Empty };
+ var reference = new ValueReference(obj, ref obj.Field);
+
+ obj.Field = 20;
+ Equal(obj.Field, reference.Value);
+
+ reference.Value = 42;
+ Equal(obj.Field, reference.Value);
+ Empty(obj.AnotherField);
+ }
+
+ [Fact]
+ public static void ImmutableFieldRef()
+ {
+ var obj = new MyClass() { AnotherField = string.Empty };
+ var reference = new ReadOnlyValueReference(obj, in obj.Field);
+
+ obj.Field = 20;
+ Equal(obj.Field, reference.Value);
+
+ Equal(obj.Field, reference.Value);
+ Empty(obj.AnotherField);
+ }
+
+ [Fact]
+ public static void MutableToImmutableRef()
+ {
+ var obj = new MyClass() { AnotherField = string.Empty };
+ var reference = new ValueReference(obj, ref obj.Field);
+ ReadOnlyValueReference roReference = reference;
+
+ obj.Field = 20;
+ Equal(roReference.Value, reference.Value);
+
+ reference.Value = 42;
+ Equal(roReference.Value, reference.Value);
+ }
+
+ [Fact]
+ public static void MutableRefEquality()
+ {
+ var obj = new MyClass() { AnotherField = string.Empty };
+ var reference1 = new ValueReference(obj, ref obj.Field);
+ var reference2 = new ValueReference(obj, ref obj.Field);
+
+ Equal(reference1, reference2);
+ }
+
+ [Fact]
+ public static void ImmutableRefEquality()
+ {
+ var obj = new MyClass() { AnotherField = string.Empty };
+ var reference1 = new ReadOnlyValueReference(obj, in obj.Field);
+ var reference2 = new ReadOnlyValueReference(obj, in obj.Field);
+
+ Equal(reference1, reference2);
+ }
+
+ [Fact]
+ public static void ReferenceToArray()
+ {
+ var array = new string[1];
+ var reference = new ValueReference(array, 0)
+ {
+ Value = "Hello, world!"
+ };
+
+ Same(array[0], reference.Value);
+ Same(array[0], reference.ToString());
+ }
+
+ [Fact]
+ public static void MutableEmptyRef()
+ {
+ var reference = default(ValueReference);
+ True(reference.IsEmpty);
+ Null(reference.ToString());
+ }
+
+ [Fact]
+ public static void ImmutableEmptyRef()
+ {
+ var reference = default(ReadOnlyValueReference);
+ True(reference.IsEmpty);
+ Null(reference.ToString());
+ }
+
+ private record class MyClass : IResettable
+ {
+ internal int Field;
+ internal string AnotherField;
+
+ public virtual void Reset()
+ {
+
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/DotNext.Threading/DotNext.Threading.csproj b/src/DotNext.Threading/DotNext.Threading.csproj
index 83214d491c..d312917eea 100644
--- a/src/DotNext.Threading/DotNext.Threading.csproj
+++ b/src/DotNext.Threading/DotNext.Threading.csproj
@@ -7,7 +7,7 @@
true
true
nullablePublicOnly
- 5.11.0
+ 5.12.0
.NET Foundation and Contributors
.NEXT Family of Libraries
diff --git a/src/DotNext.Threading/Numerics/PrimeNumber.cs b/src/DotNext.Threading/Numerics/PrimeNumber.cs
new file mode 100644
index 0000000000..f407e56a98
--- /dev/null
+++ b/src/DotNext.Threading/Numerics/PrimeNumber.cs
@@ -0,0 +1,31 @@
+using System.Diagnostics;
+
+namespace DotNext.Numerics;
+
+internal static class PrimeNumber
+{
+ private static ReadOnlySpan Primes =>
+ [
+ 3, 7, 11, 17, 23, 29, 37, 47, 59, 71, 89, 107, 131, 163, 197, 239, 293, 353, 431, 521, 631, 761, 919,
+ 1103, 1327, 1597, 1931, 2333, 2801, 3371, 4049, 4861, 5839, 7013, 8419, 10103, 12143, 14591,
+ 17519, 21023, 25229, 30293, 36353, 43627, 52361, 62851, 75431, 90523, 108631, 130363, 156437,
+ 187751, 225307, 270371, 324449, 389357, 467237, 560689, 672827, 807403, 968897, 1162687, 1395263,
+ 1674319, 2009191, 2411033, 2893249, 3471899, 4166287, 4999559, 5999471, 7199369
+ ];
+
+ internal static int GetPrime(int min) => Number.GetPrime(min, Primes);
+
+ internal static ulong GetFastModMultiplier(ulong divisor)
+ => ulong.MaxValue / divisor + 1UL;
+
+ // Daniel Lemire's fastmod algorithm: https://lemire.me/blog/2016/06/27/a-fast-alternative-to-the-modulo-reduction/
+ internal static uint FastMod(uint value, uint divisor, ulong multiplier)
+ {
+ Debug.Assert(divisor <= int.MaxValue);
+
+ var result = (uint)(((((multiplier * value) >> 32) + 1UL) * divisor) >> 32);
+ Debug.Assert(result == value % divisor);
+
+ return result;
+ }
+}
\ No newline at end of file
diff --git a/src/DotNext.Threading/Runtime/Caching/RandomAccessCache.Debug.cs b/src/DotNext.Threading/Runtime/Caching/RandomAccessCache.Debug.cs
new file mode 100644
index 0000000000..e312c644c5
--- /dev/null
+++ b/src/DotNext.Threading/Runtime/Caching/RandomAccessCache.Debug.cs
@@ -0,0 +1,21 @@
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+
+namespace DotNext.Runtime.Caching;
+
+[DebuggerDisplay($"EvictionListSize = {{{nameof(EvictionListSize)}}}, QueueSize = {{{nameof(QueueSize)}}}")]
+public partial class RandomAccessCache
+{
+ [ExcludeFromCodeCoverage]
+ [DebuggerBrowsable(DebuggerBrowsableState.Never)]
+ private (int Dead, int Alive) EvictionListSize => evictionHead?.EvictionNodesCount ?? default;
+
+ [ExcludeFromCodeCoverage]
+ [DebuggerBrowsable(DebuggerBrowsableState.Never)]
+ private int QueueSize => queueHead?.QueueLength ?? 0;
+
+ internal partial class KeyValuePair
+ {
+ protected string ToString(TValue value) => $"Key = {Key} Value = {value}, Promoted = {IsNotified}, IsAlive = {!IsDead}";
+ }
+}
\ No newline at end of file
diff --git a/src/DotNext.Threading/Runtime/Caching/RandomAccessCache.Dictionary.cs b/src/DotNext.Threading/Runtime/Caching/RandomAccessCache.Dictionary.cs
new file mode 100644
index 0000000000..cbe314f772
--- /dev/null
+++ b/src/DotNext.Threading/Runtime/Caching/RandomAccessCache.Dictionary.cs
@@ -0,0 +1,390 @@
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+namespace DotNext.Runtime.Caching;
+
+using Numerics;
+using Threading;
+
+public partial class RandomAccessCache
+{
+ // devirtualize Value getter manually (JIT will replace this method with one of the actual branches)
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static TValue GetValue(KeyValuePair pair)
+ {
+ Debug.Assert(pair is not null);
+ Debug.Assert(pair is not FakeKeyValuePair);
+ Debug.Assert(Atomic.IsAtomic() ? pair is KeyValuePairAtomicAccess : pair is KeyValuePairNonAtomicAccess);
+
+ return Atomic.IsAtomic()
+ ? Unsafe.As(pair).Value
+ : Unsafe.As(pair).Value;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static void SetValue(KeyValuePair pair, TValue value)
+ {
+ Debug.Assert(pair is not FakeKeyValuePair);
+
+ if (Atomic.IsAtomic())
+ {
+ Unsafe.As(pair).Value = value;
+ }
+ else
+ {
+ Unsafe.As(pair).Value = value;
+ }
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static void ClearValue(KeyValuePair pair)
+ {
+ Debug.Assert(pair is not FakeKeyValuePair);
+
+ if (!RuntimeHelpers.IsReferenceOrContainsReferences())
+ {
+ // do nothing
+ }
+ else if (Atomic.IsAtomic())
+ {
+ Unsafe.As(pair).Value = default!;
+ }
+ else
+ {
+ Unsafe.As(pair).ClearValue();
+ }
+ }
+
+ private static KeyValuePair CreatePair(TKey key, TValue value, int hashCode)
+ {
+ return Atomic.IsAtomic()
+ ? new KeyValuePairAtomicAccess(key, hashCode) { Value = value }
+ : new KeyValuePairNonAtomicAccess(key, hashCode) { Value = value };
+ }
+
+ private readonly Bucket[] buckets;
+ private readonly ulong fastModMultiplier;
+
+ private Bucket GetBucket(int hashCode)
+ {
+ var index = (int)(IntPtr.Size is sizeof(ulong)
+ ? PrimeNumber.FastMod((uint)hashCode, (uint)buckets.Length, fastModMultiplier)
+ : (uint)hashCode % (uint)buckets.Length);
+
+ Debug.Assert((uint)index < (uint)buckets.Length);
+
+ return Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(buckets), index);
+ }
+
+ internal partial class KeyValuePair(TKey key, int hashCode)
+ {
+ internal readonly int KeyHashCode = hashCode;
+ internal readonly TKey Key = key;
+ internal volatile KeyValuePair? NextInBucket; // volatile, used by the dictionary subsystem only
+
+ // Reference counting is used to establish lifetime of the stored value (not KeyValuePair instance).
+ // Initial value 1 means that the pair is referenced by the eviction queue. There
+ // are two competing threads that may decrement the counter to zero: removal thread (see TryRemove)
+ // and eviction thread. To synchronize the decision, 'cacheState' is used. The thread that evicts the pair
+ // successfully (transition from 0 => -1) is able to decrement the counter to zero.
+ private volatile int lifetimeCounter = 1;
+
+ internal bool TryAcquireCounter()
+ {
+ int currentValue, tmp = lifetimeCounter;
+ do
+ {
+ currentValue = tmp;
+ if (currentValue is 0)
+ break;
+ } while ((tmp = Interlocked.CompareExchange(ref lifetimeCounter, currentValue + 1, currentValue)) != currentValue);
+
+ return currentValue > 0U;
+ }
+
+ internal bool ReleaseCounter() => Interlocked.Decrement(ref lifetimeCounter) > 0;
+
+ [ExcludeFromCodeCoverage]
+ [DebuggerBrowsable(DebuggerBrowsableState.Never)]
+ internal (int Alive, int Dead) BucketNodesCount
+ {
+ get
+ {
+ var alive = 0;
+ var dead = 0;
+ for (var current = this; current is not null; current = current.NextInBucket)
+ {
+ ref var counterRef = ref current.IsDead ? ref dead : ref alive;
+ counterRef++;
+ }
+
+ return (alive, dead);
+ }
+ }
+ }
+
+ private sealed class KeyValuePairAtomicAccess(TKey key, int hashCode) : KeyValuePair(key, hashCode)
+ {
+ internal required TValue Value;
+
+ public override string ToString() => ToString(Value);
+ }
+
+ // non-atomic access utilizes copy-on-write semantics
+ private sealed class KeyValuePairNonAtomicAccess(TKey key, int hashCode) : KeyValuePair(key, hashCode)
+ {
+ private sealed class ValueHolder(TValue value)
+ {
+ internal readonly TValue Value = value;
+ }
+
+ private static readonly ValueHolder DefaultHolder = new(default!);
+
+ private ValueHolder holder;
+
+ internal required TValue Value
+ {
+ get => holder.Value;
+
+ [MemberNotNull(nameof(holder))] set => holder = new(value);
+ }
+
+ internal void ClearValue() => holder = DefaultHolder;
+
+ public override string ToString() => ToString(Value);
+ }
+
+ [DebuggerDisplay($"NumberOfItems = {{{nameof(Count)}}}")]
+ internal sealed class Bucket : AsyncExclusiveLock
+ {
+ private volatile KeyValuePair? first; // volatile
+
+ [ExcludeFromCodeCoverage]
+ private (int Alive, int Dead) Count => first?.BucketNodesCount ?? default;
+
+ internal KeyValuePair? TryAdd(IEqualityComparer? keyComparer, TKey key, int hashCode, TValue value)
+ {
+ var firstCopy = first;
+ if (firstCopy is not null && firstCopy.KeyHashCode == hashCode
+ && (keyComparer?.Equals(key, firstCopy.Key)
+ ?? EqualityComparer.Default.Equals(key, firstCopy.Key)))
+ {
+ return null;
+ }
+
+ var newPair = CreatePair(key, value, hashCode);
+ newPair.NextInBucket = firstCopy;
+ first = newPair;
+ return newPair;
+ }
+
+ private void Remove(KeyValuePair? previous, KeyValuePair current)
+ {
+ ref var location = ref previous is null ? ref first : ref previous.NextInBucket;
+ Volatile.Write(ref location, current.NextInBucket);
+ }
+
+ internal KeyValuePair? TryRemove(IEqualityComparer? keyComparer, TKey key, int hashCode)
+ {
+ var result = default(KeyValuePair?);
+
+ // remove all dead nodes from the bucket
+ if (keyComparer is null)
+ {
+ for (KeyValuePair? current = first, previous = null;
+ current is not null;
+ previous = current, current = current.NextInBucket)
+ {
+ if (result is null && hashCode == current.KeyHashCode
+ && EqualityComparer.Default.Equals(key, current.Key)
+ && current.MarkAsEvicted())
+ {
+ result = current;
+ }
+
+ if (current.IsDead)
+ {
+ Remove(previous, current);
+ }
+ }
+ }
+ else
+ {
+ for (KeyValuePair? current = first, previous = null;
+ current is not null;
+ previous = current, current = current.NextInBucket)
+ {
+ if (result is null && hashCode == current.KeyHashCode
+ && keyComparer.Equals(key, current.Key)
+ && current.MarkAsEvicted())
+ {
+ result = current;
+ }
+
+ if (current.IsDead)
+ {
+ Remove(previous, current);
+ }
+ }
+ }
+
+ return result;
+ }
+
+ internal KeyValuePair? TryGet(IEqualityComparer? keyComparer, TKey key, int hashCode)
+ {
+ var result = default(KeyValuePair?);
+
+ // remove all dead nodes from the bucket
+ if (keyComparer is null)
+ {
+ for (KeyValuePair? current = first, previous = null;
+ current is not null;
+ previous = current, current = current.NextInBucket)
+ {
+ if (result is null && hashCode == current.KeyHashCode
+ && EqualityComparer.Default.Equals(key, current.Key)
+ && current.Visit()
+ && current.TryAcquireCounter())
+ {
+ result = current;
+ }
+
+ if (current.IsDead)
+ {
+ Remove(previous, current);
+ }
+ }
+ }
+ else
+ {
+ for (KeyValuePair? current = first, previous = null;
+ current is not null;
+ previous = current, current = current.NextInBucket)
+ {
+ if (result is null && hashCode == current.KeyHashCode
+ && keyComparer.Equals(key, current.Key)
+ && current.Visit()
+ && current.TryAcquireCounter())
+ {
+ result = current;
+ }
+
+ if (current.IsDead)
+ {
+ Remove(previous, current);
+ }
+ }
+ }
+
+ return result;
+ }
+
+ internal KeyValuePair? Modify(IEqualityComparer? keyComparer, TKey key, int hashCode)
+ {
+ KeyValuePair? valueHolder = null;
+ if (keyComparer is null)
+ {
+ for (KeyValuePair? current = first, previous = null; current is not null; previous = current, current = current.NextInBucket)
+ {
+ if (valueHolder is null && hashCode == current.KeyHashCode
+ && EqualityComparer.Default.Equals(key, current.Key)
+ && current.Visit()
+ && current.TryAcquireCounter())
+ {
+ valueHolder = current;
+ }
+
+ if (current.IsDead)
+ {
+ Remove(previous, current);
+ }
+ }
+ }
+ else
+ {
+ for (KeyValuePair? current = first, previous = null; current is not null; previous = current, current = current.NextInBucket)
+ {
+ if (valueHolder is null && hashCode == current.KeyHashCode
+ && keyComparer.Equals(key, current.Key)
+ && current.Visit()
+ && current.TryAcquireCounter())
+ {
+ valueHolder = current;
+ }
+
+ if (current.IsDead)
+ {
+ Remove(previous, current);
+ }
+ }
+ }
+
+ return valueHolder;
+ }
+
+ internal void CleanUp(IEqualityComparer? keyComparer)
+ {
+ // remove all dead nodes from the bucket
+ if (keyComparer is null)
+ {
+ for (KeyValuePair? current = first, previous = null;
+ current is not null;
+ previous = current, current = current.NextInBucket)
+ {
+ if (current.IsDead)
+ {
+ Remove(previous, current);
+ }
+ }
+ }
+ else
+ {
+ for (KeyValuePair? current = first, previous = null;
+ current is not null;
+ previous = current, current = current.NextInBucket)
+ {
+ if (current.IsDead)
+ {
+ Remove(previous, current);
+ }
+ }
+ }
+ }
+
+ internal void Invalidate(IEqualityComparer? keyComparer, Action cleanup)
+ {
+ // remove all dead nodes from the bucket
+ if (keyComparer is null)
+ {
+ for (KeyValuePair? current = first, previous = null;
+ current is not null;
+ previous = current, current = current.NextInBucket)
+ {
+ Remove(previous, current);
+
+ if (current.MarkAsEvicted() && current.ReleaseCounter() is false)
+ {
+ cleanup.Invoke(current);
+ }
+ }
+ }
+ else
+ {
+ for (KeyValuePair? current = first, previous = null;
+ current is not null;
+ previous = current, current = current.NextInBucket)
+ {
+ Remove(previous, current);
+
+ if (current.MarkAsEvicted() && current.ReleaseCounter() is false)
+ {
+ cleanup.Invoke(current);
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/DotNext.Threading/Runtime/Caching/RandomAccessCache.Eviction.cs b/src/DotNext.Threading/Runtime/Caching/RandomAccessCache.Eviction.cs
new file mode 100644
index 0000000000..053d427316
--- /dev/null
+++ b/src/DotNext.Threading/Runtime/Caching/RandomAccessCache.Eviction.cs
@@ -0,0 +1,278 @@
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using System.Runtime.CompilerServices;
+using System.Threading.Tasks.Sources;
+
+namespace DotNext.Runtime.Caching;
+
+using CompilerServices;
+
+public partial class RandomAccessCache
+{
+ private readonly CancelableValueTaskCompletionSource completionSource;
+
+ // SIEVE core
+ private readonly int maxCacheSize;
+ private int currentSize;
+ private KeyValuePair? evictionHead, evictionTail, sieveHand;
+
+ [AsyncMethodBuilder(typeof(SpawningAsyncTaskMethodBuilder))]
+ private async Task DoEvictionAsync()
+ {
+ while (!IsDisposingOrDisposed)
+ {
+ if (queueHead.NextInQueue is KeyValuePair newHead)
+ {
+ queueHead.NextInQueue = Sentinel.Instance;
+ queueHead = newHead;
+ }
+ else if (await completionSource.WaitAsync(queueHead).ConfigureAwait(false))
+ {
+ continue;
+ }
+ else
+ {
+ break;
+ }
+
+ Debug.Assert(queueHead is not FakeKeyValuePair);
+ EvictOrInsert(queueHead);
+ }
+ }
+
+ private void EvictOrInsert(KeyValuePair dequeued)
+ {
+ if (currentSize == maxCacheSize)
+ Evict();
+
+ Debug.Assert(currentSize < maxCacheSize);
+ dequeued.Prepend(ref evictionHead, ref evictionTail);
+ sieveHand ??= evictionTail;
+ currentSize++;
+ }
+
+ private void Evict()
+ {
+ Debug.Assert(sieveHand is not null);
+ Debug.Assert(evictionHead is not null);
+ Debug.Assert(evictionTail is not null);
+
+ while (sieveHand is not null)
+ {
+ if (!sieveHand.Evict(out var removed))
+ {
+ sieveHand = sieveHand.MoveBackward() ?? evictionTail;
+ }
+ else
+ {
+ var removedPair = sieveHand;
+ sieveHand = sieveHand.DetachAndMoveBackward(ref evictionHead, ref evictionTail) ?? evictionTail;
+ currentSize--;
+ if (!removed && removedPair.ReleaseCounter() is false)
+ {
+ Eviction?.Invoke(removedPair.Key, GetValue(removedPair));
+ ClearValue(removedPair);
+ TryCleanUpBucket(GetBucket(removedPair.KeyHashCode));
+ break;
+ }
+ }
+ }
+ }
+
+ private void TryCleanUpBucket(Bucket bucket)
+ {
+ if (bucket.TryAcquire())
+ {
+ try
+ {
+ bucket.CleanUp(keyComparer);
+ }
+ finally
+ {
+ bucket.Release();
+ }
+ }
+ }
+
+ internal partial class KeyValuePair
+ {
+ private const int EvictedState = -1;
+ private const int NotVisitedState = 0;
+ private const int VisitedState = 1;
+
+ private (KeyValuePair? Previous, KeyValuePair? Next) sieveLinks;
+ private volatile int cacheState;
+
+ internal KeyValuePair? MoveBackward()
+ => sieveLinks.Previous;
+
+ internal void Prepend([NotNull] ref KeyValuePair? head, [NotNull] ref KeyValuePair? tail)
+ {
+ if (head is null || tail is null)
+ {
+ head = tail = this;
+ }
+ else
+ {
+ head = (sieveLinks.Next = head).sieveLinks.Previous = this;
+ }
+ }
+
+ internal KeyValuePair? DetachAndMoveBackward(ref KeyValuePair? head, ref KeyValuePair? tail)
+ {
+ var (previous, next) = sieveLinks;
+
+ if (previous is null)
+ {
+ head = next;
+ }
+
+ if (next is null)
+ {
+ tail = previous;
+ }
+
+ MakeLink(previous, next);
+ sieveLinks = default;
+ return previous;
+
+ static void MakeLink(KeyValuePair? previous, KeyValuePair? next)
+ {
+ if (previous is not null)
+ {
+ previous.sieveLinks.Next = next;
+ }
+
+ if (next is not null)
+ {
+ next.sieveLinks.Previous = previous;
+ }
+ }
+ }
+
+ internal bool Evict(out bool removed)
+ {
+ var counter = Interlocked.Decrement(ref cacheState);
+ removed = counter < EvictedState;
+ return counter < NotVisitedState;
+ }
+
+ internal bool Visit()
+ => Interlocked.CompareExchange(ref cacheState, VisitedState, NotVisitedState) >= NotVisitedState;
+
+ internal bool MarkAsEvicted()
+ => Interlocked.Exchange(ref cacheState, EvictedState) >= NotVisitedState;
+
+ internal bool IsDead => cacheState < NotVisitedState;
+
+ [ExcludeFromCodeCoverage]
+ [DebuggerBrowsable(DebuggerBrowsableState.Never)]
+ internal (int Alive, int Dead) EvictionNodesCount
+ {
+ get
+ {
+ var alive = 0;
+ var dead = 0;
+ for (var current = this; current is not null; current = current.sieveLinks.Next)
+ {
+ ref var counterRef = ref current.IsDead ? ref dead : ref alive;
+ counterRef++;
+ }
+
+ return (alive, dead);
+ }
+ }
+ }
+
+ private sealed class CancelableValueTaskCompletionSource : Disposable, IValueTaskSource, IThreadPoolWorkItem
+ {
+ private object? continuationState;
+ private volatile Action