Skip to content

Commit a3aede1

Browse files
author
Travis Whidden
committed
zzzprojects#764 - Code Review Changes implemented; ThreadSafeSlidingCache Tests Added
1 parent 579085e commit a3aede1

File tree

6 files changed

+335
-122
lines changed

6 files changed

+335
-122
lines changed

src/System.Linq.Dynamic.Core/Parser/ConstantExpressionHelper.cs

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using System.Linq.Expressions;
1+
using System.Linq.Dynamic.Core.Util;
2+
using System.Linq.Expressions;
23

34
namespace System.Linq.Dynamic.Core.Parser
45
{

src/System.Linq.Dynamic.Core/Parser/ThreadSafeSlidingCache.cs

-83
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
namespace System.Linq.Dynamic.Core.Util
2+
{
3+
internal interface IDateTimeUtils
4+
{
5+
DateTime UtcNow { get; }
6+
}
7+
8+
internal class DateTimeUtils : IDateTimeUtils
9+
{
10+
public DateTime UtcNow => DateTime.UtcNow;
11+
}
12+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
using System.Collections.Concurrent;
2+
using System.Linq.Dynamic.Core.Validation;
3+
using System.Threading.Tasks;
4+
5+
namespace System.Linq.Dynamic.Core.Util
6+
{
7+
internal class ThreadSafeSlidingCache<TKey, TValue> where TKey : notnull where TValue : notnull
8+
{
9+
// ReSharper disable once StaticMemberInGenericType
10+
private static readonly TimeSpan _defaultCleanupFrequency = TimeSpan.FromMinutes(10);
11+
private readonly ConcurrentDictionary<TKey, (TValue Value, DateTime ExpirationTime)> _cache;
12+
private readonly TimeSpan _cleanupFrequency;
13+
private readonly IDateTimeUtils _dateTimeProvider;
14+
private readonly Func<Task> _deleteExpiredCachedItemsDelegate;
15+
private readonly long? _minCacheItemsBeforeCleanup;
16+
private DateTime _lastCleanupTime = DateTime.MinValue;
17+
18+
/// <summary>
19+
/// Sliding Thread Safe Cache
20+
/// </summary>
21+
/// <param name="timeToLive">The length of time any object would survive before being removed</param>
22+
/// <param name="cleanupFrequency">Only look for expired objects over specific periods</param>
23+
/// <param name="minCacheItemsBeforeCleanup">
24+
/// If defined, only allow the cleanup process after x number of cached items have
25+
/// been stored
26+
/// </param>
27+
/// <param name="dateTimeProvider">
28+
/// Provides the Time for the Caching object. Default will be created if not supplied. Used
29+
/// for Testing classes
30+
/// </param>
31+
public ThreadSafeSlidingCache(
32+
TimeSpan timeToLive,
33+
TimeSpan? cleanupFrequency = null,
34+
long? minCacheItemsBeforeCleanup = null,
35+
IDateTimeUtils? dateTimeProvider = null)
36+
{
37+
_cache = new ConcurrentDictionary<TKey, (TValue, DateTime)>();
38+
TimeToLive = timeToLive;
39+
_minCacheItemsBeforeCleanup = minCacheItemsBeforeCleanup;
40+
_cleanupFrequency = cleanupFrequency ?? _defaultCleanupFrequency;
41+
_deleteExpiredCachedItemsDelegate = Cleanup;
42+
_dateTimeProvider = dateTimeProvider ?? new DateTimeUtils();
43+
}
44+
45+
public TimeSpan TimeToLive { get; }
46+
47+
/// <summary>
48+
/// Provide the number of items in the cache
49+
/// </summary>
50+
public int Count => _cache.Count;
51+
52+
public void AddOrUpdate(TKey key, TValue value)
53+
{
54+
Check.NotNull(key);
55+
Check.NotNull(value);
56+
57+
var expirationTime = _dateTimeProvider.UtcNow.Add(TimeToLive);
58+
_cache[key] = (value, expirationTime);
59+
60+
CleanupIfNeeded();
61+
}
62+
63+
public bool TryGetValue(TKey key, out TValue value)
64+
{
65+
Check.NotNull(key);
66+
67+
CleanupIfNeeded();
68+
69+
if (_cache.TryGetValue(key, out var valueAndExpiration))
70+
{
71+
if (_dateTimeProvider.UtcNow <= valueAndExpiration.ExpirationTime)
72+
{
73+
value = valueAndExpiration.Value;
74+
_cache[key] = (value, _dateTimeProvider.UtcNow.Add(TimeToLive));
75+
return true;
76+
}
77+
78+
// Remove expired item
79+
_cache.TryRemove(key, out _);
80+
}
81+
82+
value = default!;
83+
return false;
84+
}
85+
86+
public bool Remove(TKey key)
87+
{
88+
Check.NotNull(key);
89+
90+
var removed = _cache.TryRemove(key, out _);
91+
CleanupIfNeeded();
92+
return removed;
93+
}
94+
95+
/// <summary>
96+
/// Check if cache needs to be cleaned up.
97+
/// If it does, span the cleanup as a Task to prevent from blocking
98+
/// </summary>
99+
private void CleanupIfNeeded()
100+
{
101+
if (_dateTimeProvider.UtcNow - _lastCleanupTime > _cleanupFrequency
102+
&& (_minCacheItemsBeforeCleanup == null ||
103+
_cache.Count >=
104+
_minCacheItemsBeforeCleanup) // Only cleanup if we have a minimum number of items in the cache.
105+
)
106+
{
107+
// Set here, so we don't have re-entry due to large collection enumeration.
108+
_lastCleanupTime = _dateTimeProvider.UtcNow;
109+
110+
Task.Run(_deleteExpiredCachedItemsDelegate);
111+
}
112+
}
113+
114+
/// <summary>
115+
/// Cleanup the Cache
116+
/// </summary>
117+
/// <returns></returns>
118+
private Task Cleanup()
119+
{
120+
foreach (var key in _cache.Keys)
121+
{
122+
if (_dateTimeProvider.UtcNow > _cache[key].ExpirationTime)
123+
{
124+
_cache.TryRemove(key, out _);
125+
}
126+
}
127+
128+
return Task.CompletedTask;
129+
}
130+
}
131+
}

test/System.Linq.Dynamic.Core.Tests/Util/ConstantExpressionHelperTests.cs

+38-38
Original file line numberDiff line numberDiff line change
@@ -6,43 +6,43 @@ namespace System.Linq.Dynamic.Core.Tests
66
{
77
public partial class EntitiesTests
88
{
9-
[Fact]
10-
public async Task TestConstantExpressionLeak()
11-
{
12-
//Arrange
13-
PopulateTestData(1, 0);
14-
15-
var populateExpression = _context.Blogs.All("BlogId > 2000");
16-
17-
var expressions = ConstantExpressionHelper.Expressions;
18-
19-
// Should contain
20-
if (!expressions.TryGetValue(2000, out _))
21-
{
22-
Assert.Fail("Cache was missing constant expression for 2000");
23-
}
24-
25-
// wait half the expiry time
26-
await Task.Delay(TimeSpan.FromSeconds(ConstantExpressionHelper.Expressions.TimeToLive.TotalSeconds/2));
27-
if (!expressions.TryGetValue(2000, out _))
28-
{
29-
Assert.Fail("Cache was missing constant expression for 2000 (1)");
30-
}
31-
32-
// wait another half the expiry time, plus one second
33-
await Task.Delay(TimeSpan.FromSeconds((ConstantExpressionHelper.Expressions.TimeToLive.TotalSeconds / 2)+1));
34-
if (!expressions.TryGetValue(2000, out _))
35-
{
36-
Assert.Fail("Cache was missing constant expression for 2000 (2)");
37-
}
38-
39-
// Wait for the slide cache to expire, check on second later
40-
await Task.Delay(ConstantExpressionHelper.Expressions.TimeToLive.Add(TimeSpan.FromSeconds(1)));
41-
42-
if (expressions.TryGetValue(2000, out _))
43-
{
44-
Assert.Fail("Expected constant to be expired 2000");
45-
}
46-
}
9+
//[Fact]
10+
//public async Task TestConstantExpressionLeak()
11+
//{
12+
// //Arrange
13+
// PopulateTestData(1, 0);
14+
15+
// var populateExpression = _context.Blogs.All("BlogId > 2000");
16+
17+
// var expressions = ConstantExpressionHelper.Expressions;
18+
19+
// // Should contain
20+
// if (!expressions.TryGetValue(2000, out _))
21+
// {
22+
// Assert.Fail("Cache was missing constant expression for 2000");
23+
// }
24+
25+
// // wait half the expiry time
26+
// await Task.Delay(TimeSpan.FromSeconds(ConstantExpressionHelper.Expressions.TimeToLive.TotalSeconds/2));
27+
// if (!expressions.TryGetValue(2000, out _))
28+
// {
29+
// Assert.Fail("Cache was missing constant expression for 2000 (1)");
30+
// }
31+
32+
// // wait another half the expiry time, plus one second
33+
// await Task.Delay(TimeSpan.FromSeconds((ConstantExpressionHelper.Expressions.TimeToLive.TotalSeconds / 2)+1));
34+
// if (!expressions.TryGetValue(2000, out _))
35+
// {
36+
// Assert.Fail("Cache was missing constant expression for 2000 (2)");
37+
// }
38+
39+
// // Wait for the slide cache to expire, check on second later
40+
// await Task.Delay(ConstantExpressionHelper.Expressions.TimeToLive.Add(TimeSpan.FromSeconds(1)));
41+
42+
// if (expressions.TryGetValue(2000, out _))
43+
// {
44+
// Assert.Fail("Expected constant to be expired 2000");
45+
// }
46+
//}
4747
}
4848
}

0 commit comments

Comments
 (0)