Skip to content

Commit df9af6a

Browse files
authored
Merge pull request #1 from cajuncoding/feature/add_support_for_self_expiring_cache_results
Feature/add support for self expiring cache results
2 parents 6141102 + 9c5be79 commit df9af6a

16 files changed

+704
-116
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: 4 additions & 3 deletions
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>
@@ -66,7 +67,7 @@ public static CacheItemPolicy NewAbsoluteExpirationPolicy(TimeSpan cacheTimeSpan
6667
private static readonly ThreadLocal<Random> _threadLocalRandomGenerator = new ThreadLocal<Random>(() => new Random(Guid.NewGuid().GetHashCode()));
6768

6869
/// <summary>
69-
/// Provides logic to randomly distribute Cache TTL values within the specified threshhold to help ensure that
70+
/// Provides logic to randomly distribute Cache TTL values within the specified threshold to help ensure that
7071
/// cached values are not all exactly equal. This is helpful to provide dynamic caching that is much less likely
7172
/// to result in all elements being expired from cache at the exact same time, but balances this with the fact that
7273
/// they should expire relatively close in time. So the Threshold value should be small; e.g. with 30 seconds, or
@@ -84,7 +85,7 @@ public static TimeSpan RandomizeCacheTTLDistribution(TimeSpan timeSpanTTL, int m
8485
//BBernard
8586
//Salt the timespan with a small randomly generated offset value (e.g. spice it up a little with some salt)!
8687
//NOTE: We use a random number generator to help us salt our Cache intervals and better
87-
// stimilate offset caching of items. This is a simplistic way to help level out the
88+
// stimulate offset caching of items. This is a simplistic way to help level out the
8889
// cache hit/miss distribution so that fewer requests take the same large hit to refresh
8990
// when they expire by absolute time.
9091
//For Example: If in a high load environment multiple requests could all result in cache misses
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
using System;
2+
using System.Threading.Tasks;
3+
4+
namespace LazyCacheHelpers
5+
{
6+
/// <summary>
7+
/// BBernard
8+
/// Original Source (MIT License): https://github.com/cajuncoding/LazyCacheHelpers
9+
///
10+
/// Public Interface for the Lazy Cache Handler which adds support for the value factories to be self-managing/self-expiring
11+
/// by returning both the result and the policy! This is very useful in use cases such as loading Auth Tokens or other
12+
/// data from external APIs whereby the API response determines how long the result is valid for, and therefore it does
13+
/// not need to be refreshed/reloaded until that time has lapsed. So the logic can build a Cache Expiration policy based
14+
/// on the information in the response and create a valid CachePolicy.
15+
///
16+
/// NOTE: This is intended to be an extension of the base ILazyCacheHandler, but is separate now as a composable interface to
17+
/// to mitigate potentially breaking existing custom implementations of the ILazyCacheHandler.
18+
/// </summary>
19+
/// <typeparam name="TValue"></typeparam>
20+
public interface ILazyCacheHandlerSelfExpiring<TValue>
21+
{
22+
TValue GetOrAddFromCache<TKey>(TKey key, Func<ILazySelfExpiringCacheResult<TValue>> fnValueFactory);
23+
Task<TValue> GetOrAddFromCacheAsync<TKey>(TKey key, Func<Task<ILazySelfExpiringCacheResult<TValue>>> fnAsyncValueFactory);
24+
}
25+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
using System.Runtime.Caching;
2+
3+
namespace LazyCacheHelpers
4+
{
5+
public interface ILazySelfExpiringCacheResult<out TValue>
6+
{
7+
CacheItemPolicy CachePolicy { get; }
8+
TValue CacheItem { get; }
9+
}
10+
}

0 commit comments

Comments
 (0)