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)]