diff --git a/src/FastCache.Cached/Constants.cs b/src/FastCache.Cached/Constants.cs index 367a91b..5f0403e 100644 --- a/src/FastCache.Cached/Constants.cs +++ b/src/FastCache.Cached/Constants.cs @@ -17,9 +17,7 @@ internal static class Constants private static readonly TimeSpan DefaultQuickListInterval = TimeSpan.FromSeconds(15); private static readonly TimeSpan MaxQuickListInterval = TimeSpan.FromSeconds(60); -#if NET6_0_OR_GREATER - private static readonly Random Random = Random.Shared; -#else +#if !NET6_0_OR_GREATER private static readonly Random Random = new(); #endif @@ -73,7 +71,7 @@ public static TimeSpan FullEvictionInterval // This is necessary to avoid application stalling induced by all caches getting collected at the same time. var quickListTicks = (int)QuickListEvictionInterval.TotalMilliseconds; var delay = quickListTicks * EvictionIntervalMultiplyFactor; - var jitter = Random.Next(-quickListTicks, (quickListTicks * 2) + 1); + var jitter = GetRandomInt(-quickListTicks, (quickListTicks * 2) + 1); return TimeSpan.FromMilliseconds(delay + jitter); } } @@ -83,7 +81,7 @@ public static TimeSpan CacheStoreEvictionDelay get { var delay = (int)QuickListEvictionInterval.TotalMilliseconds; - var jitter = Random.Next(0, delay * 2); + var jitter = GetRandomInt(0, delay * 2); return TimeSpan.FromMilliseconds(delay + jitter); } } @@ -94,4 +92,16 @@ public static TimeSpan CacheStoreEvictionDelay public static readonly TimeSpan CooldownDelayAfterFullGC = QuickListEvictionInterval.MultiplyBy(4); private static string? GetVar(string key) => Environment.GetEnvironmentVariable(key); + + private static int GetRandomInt(int minValue, int maxValue) + { +#if NET6_0_OR_GREATER + return Random.Shared.Next(minValue, maxValue); +#else + lock (Random) + { + return Random.Next(minValue, maxValue); + } +#endif + } } diff --git a/tests/FastCache.CachedTests/Internals/CacheManager.cs b/tests/FastCache.CachedTests/Internals/CacheManager.cs index 47413c9..e9a22ae 100644 --- a/tests/FastCache.CachedTests/Internals/CacheManager.cs +++ b/tests/FastCache.CachedTests/Internals/CacheManager.cs @@ -8,7 +8,9 @@ public sealed class CacheManagerTests { private record ExpiredEntry(string Value); private record OptionallyExpiredEntry(string Value, bool IsExpired); + private record RemovableEntry(string Value); + private static readonly Random Random = new(); private static readonly TimeSpan DelayTolerance = TimeSpan.FromMilliseconds(100); [Theory] @@ -28,7 +30,7 @@ public void Trim_Throws_OnInvalidPercentage(double percentage) } [Fact] - public async Task ImmediateFullEviction_CorrectlyEvictsEntries_AllExpired() + public async Task QueueFullEviction_CorrectlyEvictsEntries_AllExpired() { CacheManager.SuspendEviction(); @@ -55,7 +57,7 @@ public async Task ImmediateFullEviction_CorrectlyEvictsEntries_AllExpired() } [Fact] - public async Task ImmediateFullEviction_CorrectlyEvictsEntries_SomeExpired() + public async Task QueueFullEviction_CorrectlyEvictsEntries_SomeExpired() { CacheManager.SuspendEviction(); @@ -89,4 +91,31 @@ public async Task ImmediateFullEviction_CorrectlyEvictsEntries_SomeExpired() Assert.False(inner.Value.IsExpired); } } + + [Fact] + public async Task QueueFullClear_Clears() + { + CacheManager.SuspendEviction(); + + var quickList = CacheStaticHolder.QuickList; + var store = CacheStaticHolder.Store; + + var length = Constants.QuickListMinLength * 2; + for (var i = 0; i < length; i++) + { + var expiration = TimeSpan.FromMilliseconds(Random.Next(1, int.MaxValue)); + + new RemovableEntry(GetRandomString()).Cache(i.ToString(), expiration); + } + + Assert.Equal((uint)Constants.QuickListMinLength, quickList.AtomicCount); + Assert.Equal(length, store.Count); + + CacheManager.QueueFullClear(); + + await Task.Delay(DelayTolerance); + + Assert.Equal(0u, quickList.AtomicCount); + Assert.Empty(store); + } } diff --git a/tests/FastCache.CachedTests/TestHelpers.cs b/tests/FastCache.CachedTests/TestHelpers.cs index 640daff..f133a63 100644 --- a/tests/FastCache.CachedTests/TestHelpers.cs +++ b/tests/FastCache.CachedTests/TestHelpers.cs @@ -16,21 +16,11 @@ public static string GetTestKey(T arg1, [CallerMemberName] string testName = return $"{testName}:{arg1}"; } - public static string GetTestKey(T1 arg1, T2 arg2, [CallerMemberName] string testName = "") - { - return testName + string.Join(':', arg1, arg2); - } - public static string GetTestKey(T1 arg1, T2 arg2, T3 arg3, [CallerMemberName] string testName = "") { return testName + string.Join(':', arg1, arg2, arg3); } - public static string GetTestKey(T1 arg1, T2 arg2, T3 arg3, T4 arg4, [CallerMemberName] string testName = "") - { - return testName + string.Join(':', arg1, arg2, arg3, arg4); - } - public static string GetRandomString() { #if !NETSTANDARD2_0 @@ -38,36 +28,11 @@ public static string GetRandomString() #else var bytes = new byte[64]; #endif - - _random.NextBytes(bytes); + lock (_random) + { + _random.NextBytes(bytes); + } return Convert.ToBase64String(bytes); } - - public static long GetRandomLong() - { -#if !NETSTANDARD2_0 - var bytes = (stackalloc byte[8]); -#else - var bytes = new byte[8]; -#endif - - _random.NextBytes(bytes); - -#if !NETSTANDARD2_0 - return BitConverter.ToInt64(bytes); -#else - return BitConverter.ToInt64(bytes, 0); -#endif - } - - public static void Unreachable() - { - throw new InvalidOperationException("This part of code should never be reached"); - } - - public static T Unreachable() - { - throw new InvalidOperationException("This part of code should never be reached"); - } }