diff --git a/.github/workflows/dotnet-releaser.yml b/.github/workflows/dotnet-releaser.yml
index 315c512..ed69c84 100644
--- a/.github/workflows/dotnet-releaser.yml
+++ b/.github/workflows/dotnet-releaser.yml
@@ -28,6 +28,7 @@ jobs:
env:
FASTCACHE_CONSIDER_GC: true
FASTCACHE_GC_THRESHOLD: 1024
+ FASTCACHE_PARALLEL_EVICTION_THRESHOLD: 2048
FASTCACHE_QUICKLIST_EVICTION_INTERVAL: 00:00:01
run: |
dotnet tool install -g dotnet-releaser
diff --git a/src/FastCache.Cached/CacheManager.cs b/src/FastCache.Cached/CacheManager.cs
index 953dbec..689ff44 100644
--- a/src/FastCache.Cached/CacheManager.cs
+++ b/src/FastCache.Cached/CacheManager.cs
@@ -10,10 +10,20 @@ public static class CacheManager
private static long s_AggregatedEvictionsCount;
///
- /// Submit full eviction for specified Cached value
+ /// Total atomic count of entries present in cache, including expired.
+ ///
+ /// Cache entry key type. string, int or (int, int) for multi-key.
+ /// Cache entry value type
+ public static int TotalCount() where K : notnull => CacheStaticHolder.Store.Count;
+
+ ///
+ /// Trigger full eviction for expired cache entries of type Cached[K, V]
///
public static void QueueFullEviction() where K : notnull => QueueFullEviction(triggeredByTimer: true);
+ ///
+ /// Remove all cache entries of type Cached[K, V] from the cache
+ ///
public static void QueueFullClear() where K : notnull
{
ThreadPool.QueueUserWorkItem(async static _ =>
@@ -37,11 +47,28 @@ public static void QueueFullClear() where K : notnull
});
}
+ ///
+ /// Enumerates all not expired entries currently present in the cache.
+ /// Cache changes introduced from other threads may not be visible to the enumerator.
+ ///
+ /// Cache entry key type. string, int or (int, int) for multi-key.
+ /// Cache entry value type
+ public static IEnumerable> EnumerateEntries() where K : notnull
+ {
+ foreach (var (key, inner) in CacheStaticHolder.Store)
+ {
+ if (inner.IsNotExpired())
+ {
+ yield return new(key, inner.Value, found: true);
+ }
+ }
+ }
+
///
/// Trims cache store for a given percentage of its size. Will remove at least 1 item.
///
- ///
- ///
+ /// Cache entry key type. string, int or (int, int) for multi-key.
+ /// Cache entry value type
///
/// True: trim is performed inline. False: the count to trim is above threshold and removal is queued to thread pool.
public static bool Trim(double percentage) where K : notnull
diff --git a/src/FastCache.Cached/Cached.cs b/src/FastCache.Cached/Cached.cs
index eb72845..096efb0 100644
--- a/src/FastCache.Cached/Cached.cs
+++ b/src/FastCache.Cached/Cached.cs
@@ -12,9 +12,16 @@ namespace FastCache;
[StructLayout(LayoutKind.Auto)]
public readonly record struct Cached where K : notnull
{
- private readonly K _key;
private readonly bool _found;
+ ///
+ /// Cache entry key. Either a single-argument like string, int, etc. or multi-key value as tuple like (int, int, bool).
+ ///
+ public readonly K Key;
+
+ ///
+ /// Cache entry value. Guaranteed to be up-to-date with millisecond accuracy as long as 'bool TryGet' conditional is checked.
+ ///
public readonly V Value;
///
@@ -26,9 +33,9 @@ namespace FastCache;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal Cached(K key, V value, bool found)
{
- _key = key;
_found = found;
+ Key = key;
Value = value;
}
@@ -42,12 +49,12 @@ public V Save(V value, TimeSpan expiration)
{
var (timestamp, milliseconds) = TimeUtils.GetTimestamp(expiration);
- CacheStaticHolder.Store[_key] = new(value, timestamp);
+ CacheStaticHolder.Store[Key] = new(value, timestamp);
CacheStaticHolder.EvictionJob.ReportExpiration(milliseconds);
if (!_found)
{
- CacheStaticHolder.QuickList.Add(_key, timestamp);
+ CacheStaticHolder.QuickList.Add(Key, timestamp);
}
return value;
@@ -81,9 +88,9 @@ public bool Update(V value)
{
var store = CacheStaticHolder.Store;
- if (_found && store.TryGetValue(_key, out var inner))
+ if (_found && store.TryGetValue(Key, out var inner))
{
- store[_key] = new(value, inner._timestamp);
+ store[Key] = new(value, inner._timestamp);
return true;
}
@@ -95,7 +102,7 @@ public bool Update(V value)
///
public void Remove()
{
- CacheStaticHolder.Store.TryRemove(_key, out _);
+ CacheStaticHolder.Store.TryRemove(Key, out _);
}
}
diff --git a/src/FastCache.Cached/FastCache.Cached.csproj b/src/FastCache.Cached/FastCache.Cached.csproj
index 0f2356d..904cd32 100644
--- a/src/FastCache.Cached/FastCache.Cached.csproj
+++ b/src/FastCache.Cached/FastCache.Cached.csproj
@@ -1,4 +1,4 @@
-
+
neon-sunset
@@ -7,6 +7,7 @@
Cache;Caching;Lock-Free;Performance;MemoryCache;In-Memory;High-Load
cached-small-transparent.png
README.md
+ true
The fastest cache library written in C# for items with set expiration time. Easy to use, thread-safe and light on memory.
Optimized to scale from dozens to millions of items. Features lock-free reads and writes, allocation-free reads and automatic eviction.
Credit to Vladimir Sadov for his implementation of NonBlocking.ConcurrentDictionary which is used as an underlying store.
diff --git a/tests/FastCache.CachedTests/FastCache.CachedTests.csproj b/tests/FastCache.CachedTests/FastCache.CachedTests.csproj
index e922054..d2b979e 100644
--- a/tests/FastCache.CachedTests/FastCache.CachedTests.csproj
+++ b/tests/FastCache.CachedTests/FastCache.CachedTests.csproj
@@ -11,6 +11,7 @@
+
diff --git a/tests/FastCache.CachedTests/Internals/CacheManager.cs b/tests/FastCache.CachedTests/Internals/CacheManager.cs
index 5d77f2c..bf0f64f 100644
--- a/tests/FastCache.CachedTests/Internals/CacheManager.cs
+++ b/tests/FastCache.CachedTests/Internals/CacheManager.cs
@@ -2,6 +2,7 @@
using FastCache.Extensions;
using FastCache.Services;
using FastCache.Helpers;
+using System.Linq;
namespace FastCache.CachedTests.Internals;
@@ -10,10 +11,44 @@ public sealed class CacheManagerTests
private record ExpiredEntry(string Value);
private record OptionallyExpiredEntry(string Value, bool IsExpired);
private record RemovableEntry(string Value);
+ private record TotalCountEntry(int Value);
+ private record EnumerableEntry();
private static readonly Random Random = new();
private static readonly TimeSpan DelayTolerance = TimeSpan.FromMilliseconds(100);
+ [Fact]
+ public void TotalCount_ReturnsCorrectValue()
+ {
+ const int expectedCount = 32768;
+
+ var entries = (0..32768)
+ .AsEnumerable()
+ .Select(i => (i, new TotalCountEntry(i)));
+
+ CachedRange.Save(entries, TimeSpan.MaxValue);
+
+ Assert.Equal(expectedCount, CacheManager.TotalCount());
+ }
+
+ [Fact]
+ public async Task EnumerateEntries_ReturnsCorrectValues()
+ {
+ var expired = (0..1024).AsEnumerable().ToDictionary(i => i, _ => new EnumerableEntry());
+ var notExpired = (1024..2048).AsEnumerable().ToDictionary(i => i, _ => new EnumerableEntry());
+
+ CachedRange.Save(expired.Select(kvp => (kvp.Key, kvp.Value)), DelayTolerance);
+ CachedRange.Save(notExpired.Select(kvp => (kvp.Key, kvp.Value)), DelayTolerance * 2);
+
+ await Task.Delay(DelayTolerance);
+
+ foreach (var cached in CacheManager.EnumerateEntries())
+ {
+ Assert.False(expired.ContainsKey(cached.Key));
+ Assert.True(notExpired.ContainsKey(cached.Key));
+ }
+ }
+
[Theory]
[InlineData(double.NaN)]
[InlineData(double.MinValue)]