Skip to content

Commit 9c5be79

Browse files
committed
- Added full working support for Self Expiring cache results that are still fully blocking/self-populating with the support of the awesome AsyncEx libraries AsyncReaderWriterLock class!
- Added support to now easily inject/bootstrap the DefaultLazyCache static implementation with your own ILazyCacheRepository, eliminating the need to have your own Static implementaiton if you don't want to duplicate it; though encapsulating in your own static facade is usually a good idea. - Implemented IDisposable support for existing LazyCacheHandler and LazyCacheRepositories to support better cleanup of resources. - Added Unit tests for self-expiring sync loading, async loading, and thread safety tests for both sync & async also! - Updated Unit Test project to net6 now.
1 parent cdd8923 commit 9c5be79

15 files changed

+504
-120
lines changed

LazyCacheHelpers.Tests/AsyncTests.cs

Lines changed: 95 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,27 @@ public async Task TestCacheHitsAsync()
4444
Assert.AreSame(result1, result4);
4545
}
4646

47+
[TestMethod]
48+
public async Task TestCacheHitsForSelfExpiringResultsAsync()
49+
{
50+
string key = $"CachedDataWithSameKey[{nameof(GetTestDataWithCachingForSelfExpiringResultsAsync)}]";
51+
52+
var result1 = await GetTestDataWithCachingForSelfExpiringResultsAsync(key);
53+
var result2 = await GetTestDataWithCachingForSelfExpiringResultsAsync(key);
54+
var result3 = await GetTestDataWithCachingForSelfExpiringResultsAsync(key);
55+
var result4 = await GetTestDataWithCachingForSelfExpiringResultsAsync(key);
56+
57+
Assert.AreEqual(result1, result2);
58+
Assert.AreSame(result1, result2);
59+
60+
Assert.AreEqual(result3, result4);
61+
Assert.AreSame(result3, result4);
62+
63+
//Compare First and Last to ensure that ALL are the same!
64+
Assert.AreEqual(result1, result4);
65+
Assert.AreSame(result1, result4);
66+
}
67+
4768
[TestMethod]
4869
public async Task TestCacheMissesAsync()
4970
{
@@ -63,6 +84,24 @@ await GetTestDataWithCachingAsync($"{key}[{++c}]")
6384
Assert.AreEqual(results.Count, distinctCount);
6485
}
6586

87+
[TestMethod]
88+
public async Task TestCacheMissesForSelfExpiringResultsAsync()
89+
{
90+
int c = 0;
91+
string key = $"CachedDataWithDifferentKey[{nameof(TestCacheMissesAsync)}]";
92+
var results = new List<string>()
93+
{
94+
await GetTestDataWithCachingForSelfExpiringResultsAsync($"{key}[{++c}]"),
95+
await GetTestDataWithCachingForSelfExpiringResultsAsync($"{key}[{++c}]"),
96+
await GetTestDataWithCachingForSelfExpiringResultsAsync($"{key}[{++c}]"),
97+
await GetTestDataWithCachingForSelfExpiringResultsAsync($"{key}[{++c}]")
98+
};
99+
100+
var distinctCount = results.Distinct().Count();
101+
102+
//Ensure that ALL Items are Distinctly Different!
103+
Assert.AreEqual(results.Count, distinctCount);
104+
}
66105

67106
[TestMethod]
68107
public async Task TestCacheMissesBecauseOfDisabledPolicyAsync()
@@ -164,7 +203,53 @@ public async Task TestCacheThreadSafetyWithLazyInitializationAsync()
164203
// meaning globalCount is only ever incremented 1 time!
165204
Assert.AreEqual(1, globalCount);
166205

167-
//Ensure ONLY ONE item was ever generated and ALL other's were identical from Cache!
206+
//Ensure ONLY ONE item was ever generated and ALL others were identical from Cache!
207+
Assert.AreEqual(1, distinctCount);
208+
209+
//Ensure that the Total time takes barely longer than one iteration of the Long Running Task!
210+
Assert.IsTrue(timer.ElapsedMilliseconds < (LongRunningTaskMillis * 2));
211+
}
212+
213+
[TestMethod]
214+
public async Task TestCacheThreadSafetyForSelfExpiringResultsAsync()
215+
{
216+
string key = $"CachedDataWithSameKey[{nameof(TestCacheThreadSafetyForSelfExpiringResultsAsync)}]";
217+
int secondsTTL = 300;
218+
int threadCount = 1000;
219+
var globalCount = 0;
220+
var timer = Stopwatch.StartNew();
221+
222+
var tasks = new List<Task<string>>();
223+
for (int x = 0; x < threadCount; x++)
224+
{
225+
//Simulated MANY threads running at the same time attempting to get the same data for the same Cache key!!!
226+
tasks.Add(Task.Run(async () =>
227+
{
228+
//THIS RUNS ON IT'S OWN THREAD, but the Lazy Cache initialization will ensure that the Value Factory Function
229+
// is only executed by the FIRST thread, and all other threads will immediately benefit from the result!
230+
return await TestCacheFacade.GetCachedSelfExpiringDataAsync(new TestCacheParams(key), async () =>
231+
{
232+
//TEST that this Code ONLY ever runs ONE TIME by ONE THREAD via Lazy<> initialization!
233+
// meaning globalCount is only ever incremented 1 time!
234+
Interlocked.Increment(ref globalCount);
235+
236+
//TEST that the cached data is never re-generated so only ONE Value is ever created!
237+
var longTaskResult = await SomeLongRunningMethodAsync(DateTime.Now);
238+
return LazySelfExpiringCacheResult.From(longTaskResult, secondsTTL);
239+
});
240+
}));
241+
}
242+
243+
//Allow all threads to complete and get the results...
244+
var results = await Task.WhenAll(tasks.ToArray());
245+
var distinctCount = results.Distinct().Count();
246+
timer.Stop();
247+
248+
//TEST that this Code ONLY ever runs ONE TIME by ONE THREAD via Lazy<> initialization!
249+
// meaning globalCount is only ever incremented 1 time!
250+
Assert.AreEqual(1, globalCount);
251+
252+
//Ensure ONLY ONE item was ever generated and ALL others were identical from Cache!
168253
Assert.AreEqual(1, distinctCount);
169254

170255
//Ensure that the Total time takes barely longer than one iteration of the Long Running Task!
@@ -183,6 +268,15 @@ public static async Task<string> GetTestDataWithCachingAsync(string key, CacheIt
183268
});
184269
}
185270

271+
public static async Task<string> GetTestDataWithCachingForSelfExpiringResultsAsync(string key, int secondsTTL = 5, bool isLongRunning = true)
272+
{
273+
return await TestCacheFacade.GetCachedSelfExpiringDataAsync(new TestCacheParams(key), async () =>
274+
{
275+
var cacheResult = await SomeLongRunningMethodAsync(DateTime.Now);
276+
return LazySelfExpiringCacheResult.From(cacheResult, secondsTTL);
277+
});
278+
}
279+
186280
public static async Task<string> GetTestDataWithCachingAndTTLAsync(string key, int secondsTTL)
187281
{
188282
return await TestCacheFacade.GetCachedDataAsync(

LazyCacheHelpers.Tests/LazyCacheHelpers.Tests.csproj

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
4-
<TargetFramework>netcoreapp3.1</TargetFramework>
4+
<TargetFramework>net6</TargetFramework>
55

66
<IsPackable>false</IsPackable>
77
</PropertyGroup>
@@ -29,8 +29,8 @@
2929
</ItemGroup>
3030

3131
<ItemGroup>
32-
<None Update="testhost.x86.dll.config">
33-
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
32+
<None Update="testhost.dll.config">
33+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
3434
</None>
3535
</ItemGroup>
3636

LazyCacheHelpers.Tests/SyncTests.cs

Lines changed: 97 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,27 @@ public void TestCacheHits()
4343
Assert.AreSame(result1, result4);
4444
}
4545

46+
[TestMethod]
47+
public void TestCacheHitsForSelfExpiringResults()
48+
{
49+
string key = $"CachedDataWithSameKey[{nameof(TestCacheHitsForSelfExpiringResults)}]";
50+
51+
var result1 = GetTestDataWithCachingForSelfExpiringResults(key);
52+
var result2 = GetTestDataWithCachingForSelfExpiringResults(key);
53+
var result3 = GetTestDataWithCachingForSelfExpiringResults(key);
54+
var result4 = GetTestDataWithCachingForSelfExpiringResults(key);
55+
56+
Assert.AreEqual(result1, result2);
57+
Assert.AreSame(result1, result2);
58+
59+
Assert.AreEqual(result3, result4);
60+
Assert.AreSame(result3, result4);
61+
62+
//Compare First and Last to ensure that ALL are the same!
63+
Assert.AreEqual(result1, result4);
64+
Assert.AreSame(result1, result4);
65+
}
66+
4667
[TestMethod]
4768
public void TestCacheMisses()
4869
{
@@ -62,6 +83,25 @@ public void TestCacheMisses()
6283
Assert.AreEqual(results.Count, distinctCount);
6384
}
6485

86+
[TestMethod]
87+
public void TestCacheMissesForSelfExpiringResults()
88+
{
89+
int c = 0;
90+
string key = $"CachedDataWithDifferentKey[{nameof(TestCacheMisses)}]";
91+
var results = new List<string>()
92+
{
93+
GetTestDataWithCachingForSelfExpiringResults($"{key}[{++c}]"),
94+
GetTestDataWithCachingForSelfExpiringResults($"{key}[{++c}]"),
95+
GetTestDataWithCachingForSelfExpiringResults($"{key}[{++c}]"),
96+
GetTestDataWithCachingForSelfExpiringResults($"{key}[{++c}]")
97+
};
98+
99+
var distinctCount = results.Distinct().Count();
100+
101+
//Ensure that ALL Items are Distinctly Different!
102+
Assert.AreEqual(results.Count, distinctCount);
103+
}
104+
65105
[TestMethod]
66106
public void TestCacheMissesBecauseOfDisabledPolicy()
67107
{
@@ -145,7 +185,6 @@ public void TestCacheRemoval()
145185
Assert.AreNotSame(result1, result3);
146186
}
147187

148-
149188
[TestMethod]
150189
public void TestCacheCountAndClearing()
151190
{
@@ -243,7 +282,53 @@ public async Task TestCacheThreadSafetyWithLazyInitialization()
243282
// meaning globalCount is only ever incremented 1 time!
244283
Assert.AreEqual(1, globalCount);
245284

246-
//Ensure ONLY ONE item was ever generated and ALL other's were identical from Cache!
285+
//Ensure ONLY ONE item was ever generated and ALL others were identical from Cache!
286+
Assert.AreEqual(1, distinctCount);
287+
288+
//Ensure that the Total time takes barely longer than one iteration of the Long Running Task!
289+
Assert.IsTrue(timer.ElapsedMilliseconds < (LongRunningTaskMillis * 2));
290+
}
291+
292+
[TestMethod]
293+
public async Task TestCacheThreadSafetyForSelfExpiringCacheResults()
294+
{
295+
string key = $"CachedDataWithSameKey[{nameof(TestCacheThreadSafetyForSelfExpiringCacheResults)}]";
296+
int secondsTTL = 300;
297+
int threadCount = 1000;
298+
var globalCount = 0;
299+
var timer = Stopwatch.StartNew();
300+
301+
var tasks = new List<Task<string>>();
302+
for (int x = 0; x < threadCount; x++)
303+
{
304+
//Simulated MANY threads running at the same time attempting to get the same data for the same Cache key!!!
305+
tasks.Add(Task.Run(() =>
306+
{
307+
//THIS RUNS ON IT'S OWN THREAD, but the Lazy Cache initialization will ensure that the Value Factory Function
308+
// is only executed by the FIRST thread, and all other threads will immediately benefit from the result!
309+
return TestCacheFacade.GetCachedSelfExpiringData(new TestCacheParams(key), () =>
310+
{
311+
//TEST that this Code ONLY ever runs ONE TIME by ONE THREAD via Lazy<> initialization!
312+
// meaning globalCount is only ever incremented 1 time!
313+
Interlocked.Increment(ref globalCount);
314+
315+
//TEST that the cached data is never re-generated so only ONE Value is ever created!
316+
var longTaskResult = SomeLongRunningMethod(DateTime.Now);
317+
return LazySelfExpiringCacheResult.From(longTaskResult, secondsTTL);
318+
});
319+
}));
320+
}
321+
322+
//Allow all threads to complete and get the results...
323+
var results = await Task.WhenAll(tasks.ToArray());
324+
var distinctCount = results.Distinct().Count();
325+
timer.Stop();
326+
327+
//TEST that this Code ONLY ever runs ONE TIME by ONE THREAD via Lazy<> initialization!
328+
// meaning globalCount is only ever incremented 1 time!
329+
Assert.AreEqual(1, globalCount);
330+
331+
//Ensure ONLY ONE item was ever generated and ALL others were identical from Cache!
247332
Assert.AreEqual(1, distinctCount);
248333

249334
//Ensure that the Total time takes barely longer than one iteration of the Long Running Task!
@@ -262,7 +347,16 @@ public static string GetTestDataWithCaching(string key, CacheItemPolicy override
262347
});
263348
}
264349

265-
public static string GetTestDataWithCachingAndTTL(string key, int secondsTTL)
350+
public static string GetTestDataWithCachingForSelfExpiringResults(string key, int secondsTTL = 5, bool isLongRunning = true)
351+
{
352+
return TestCacheFacade.GetCachedSelfExpiringData(new TestCacheParams(key), () =>
353+
{
354+
var cacheResult = SomeLongRunningMethod(DateTime.Now, isLongRunning);
355+
return LazySelfExpiringCacheResult.From(cacheResult, secondsTTL);
356+
});
357+
}
358+
359+
public static string GetTestDataWithCachingAndTTL(string key, int secondsTTL = 5)
266360
{
267361
return TestCacheFacade.GetCachedData(
268362
new TestCacheParams(key, secondsTTL),

LazyCacheHelpers.Tests/TestCacheFacade/TestCacheFacade.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,12 @@ public static string GetCachedData(ILazyCacheParams cacheParams, Func<string> fn
2121
return result;
2222
}
2323

24+
public static string GetCachedSelfExpiringData(ILazyCacheParams cacheParams, Func<ILazySelfExpiringCacheResult<string>> fnSelfExpiringValueFactory)
25+
{
26+
var result = DefaultLazyCache.GetOrAddFromCache(cacheParams, fnSelfExpiringValueFactory);
27+
return result;
28+
}
29+
2430
public static async Task<string> GetCachedDataAsync(ILazyCacheParams cacheParams, Func<Task<string>> fnValueFactory)
2531
{
2632
var result = await DefaultLazyCache.GetOrAddFromCacheAsync<ILazyCacheKey, string>(
@@ -32,6 +38,12 @@ public static async Task<string> GetCachedDataAsync(ILazyCacheParams cacheParams
3238
return result;
3339
}
3440

41+
public static async Task<string> GetCachedSelfExpiringDataAsync(ILazyCacheParams cacheParams, Func<Task<ILazySelfExpiringCacheResult<string>>> fnSelfExpiringValueFactory)
42+
{
43+
var result = await DefaultLazyCache.GetOrAddFromCacheAsync(cacheParams, fnSelfExpiringValueFactory);
44+
return result;
45+
}
46+
3547
public static void RemoveCachedData(ILazyCacheKey cacheKey)
3648
{
3749
DefaultLazyCache.RemoveFromCache(cacheKey);
File renamed without changes.

LazyCacheHelpers/CacheHelpers/LazyCachePolicy.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,10 @@ public class LazyCachePolicy
1818
/// Static Reference to a CacheItemPolicy that represents a completely Disabled cache with an AbsoluteExpiration of DateTimeOffset.MinValue (e.g. beginning-of-time).
1919
/// </summary>
2020
public static CacheItemPolicy DisabledCachingPolicy => new CacheItemPolicy() { AbsoluteExpiration = DateTimeOffset.MinValue };
21+
public static CacheItemPolicy InfiniteCachingPolicy => new CacheItemPolicy() { AbsoluteExpiration = DateTimeOffset.MaxValue };
2122

2223
/// <summary>
23-
/// Helper method to abstract logic for determining if a CacheItemPolicy is disabled (e.g. AbsoluteExpiration of DateTimeOffset.MinValue).
24+
/// Helper method to abstract logic for determining if a CacheItemPolicy is disabled or same as LazyCachePolicy.DisabledCachingPolicy (e.g. AbsoluteExpiration of DateTimeOffset.MinValue).
2425
/// </summary>
2526
/// <param name="policy"></param>
2627
/// <returns></returns>

LazyCacheHelpers/CacheInterfaces/ILazyCacheHandlerSelfExpiringResults.cs renamed to LazyCacheHelpers/CacheInterfaces/ILazyCacheHandlerSelfExpiring.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
using System;
2-
using System.Runtime.Caching;
32
using System.Threading.Tasks;
43

54
namespace LazyCacheHelpers
@@ -18,7 +17,7 @@ namespace LazyCacheHelpers
1817
/// to mitigate potentially breaking existing custom implementations of the ILazyCacheHandler.
1918
/// </summary>
2019
/// <typeparam name="TValue"></typeparam>
21-
public interface ILazyCacheHandlerSelfExpiringResults<TValue>
20+
public interface ILazyCacheHandlerSelfExpiring<TValue>
2221
{
2322
TValue GetOrAddFromCache<TKey>(TKey key, Func<ILazySelfExpiringCacheResult<TValue>> fnValueFactory);
2423
Task<TValue> GetOrAddFromCacheAsync<TKey>(TKey key, Func<Task<ILazySelfExpiringCacheResult<TValue>>> fnAsyncValueFactory);

0 commit comments

Comments
 (0)