Skip to content

Commit 4ce4385

Browse files
author
Travis Whidden
committed
zzzprojects#764 - Introduce Sliding Cache to Constant Expression Helper
1 parent 2aecd4b commit 4ce4385

File tree

3 files changed

+130
-10
lines changed

3 files changed

+130
-10
lines changed
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,18 @@
1-
using System.Collections.Concurrent;
2-
using System.Linq.Expressions;
1+
using System.Linq.Expressions;
32

43
namespace System.Linq.Dynamic.Core.Parser
54
{
65
internal static class ConstantExpressionHelper
76
{
8-
private static readonly ConcurrentDictionary<object, Expression> Expressions = new();
9-
private static readonly ConcurrentDictionary<Expression, string> Literals = new();
7+
#if DEBUG
8+
private static readonly TimeSpan TimeToLivePeriod = TimeSpan.FromSeconds(10);
9+
#else
10+
private static readonly TimeSpan TimeToLivePeriod = TimeSpan.FromMinutes(10);
11+
#endif
12+
13+
public static readonly ThreadSafeSlidingCache<object, Expression> Expressions = new(TimeToLivePeriod);
14+
private static readonly ThreadSafeSlidingCache<Expression, string> Literals = new(TimeToLivePeriod);
15+
1016

1117
public static bool TryGetText(Expression expression, out string? text)
1218
{
@@ -15,15 +21,17 @@ public static bool TryGetText(Expression expression, out string? text)
1521

1622
public static Expression CreateLiteral(object value, string text)
1723
{
18-
if (!Expressions.ContainsKey(value))
24+
if (Expressions.TryGetValue(value, out var outputValue))
1925
{
20-
ConstantExpression constantExpression = Expression.Constant(value);
21-
22-
Expressions.TryAdd(value, constantExpression);
23-
Literals.TryAdd(constantExpression, text);
26+
return outputValue;
2427
}
2528

26-
return Expressions[value];
29+
ConstantExpression constantExpression = Expression.Constant(value);
30+
31+
Expressions.AddOrUpdate(value, constantExpression);
32+
Literals.AddOrUpdate(constantExpression, text);
33+
34+
return constantExpression;
2735
}
2836
}
2937
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
using System.Collections.Concurrent;
2+
3+
namespace System.Linq.Dynamic.Core.Parser
4+
{
5+
internal class ThreadSafeSlidingCache<T1, T2> where T1 : notnull where T2 : notnull
6+
{
7+
private readonly ConcurrentDictionary<T1, (T2 Value, DateTime ExpirationTime)> _cache;
8+
private readonly TimeSpan _timeToLive;
9+
private readonly TimeSpan _cleanupFrequency;
10+
private DateTime _lastCleanupTime = DateTime.MinValue;
11+
12+
public ThreadSafeSlidingCache(TimeSpan timeToLive, TimeSpan? cleanupFrequency = null)
13+
{
14+
_cache = new ConcurrentDictionary<T1, (T2, DateTime)>();
15+
_timeToLive = timeToLive;
16+
_cleanupFrequency = cleanupFrequency ?? TimeSpan.FromSeconds(10);
17+
}
18+
19+
public TimeSpan TimeToLive => _timeToLive;
20+
21+
public void AddOrUpdate(T1 key, T2 value)
22+
{
23+
if (key == null) throw new ArgumentNullException(nameof(key));
24+
if (value == null) throw new ArgumentNullException(nameof(value));
25+
26+
var expirationTime = DateTime.UtcNow.Add(_timeToLive);
27+
_cache[key] = (value, expirationTime);
28+
29+
CleanupIfNeeded();
30+
}
31+
32+
public bool TryGetValue(T1 key, out T2 value)
33+
{
34+
if (key == null) throw new ArgumentNullException(nameof(key));
35+
36+
CleanupIfNeeded();
37+
38+
if (_cache.TryGetValue(key, out var valueAndExpiration))
39+
{
40+
if (DateTime.UtcNow <= valueAndExpiration.ExpirationTime)
41+
{
42+
value = valueAndExpiration.Value;
43+
return true;
44+
}
45+
else
46+
{
47+
// Remove expired item
48+
_cache.TryRemove(key, out _);
49+
}
50+
}
51+
52+
value = default!;
53+
return false;
54+
}
55+
56+
public bool Remove(T1 key)
57+
{
58+
if (key == null) throw new ArgumentNullException(nameof(key));
59+
var removed = _cache.TryRemove(key, out _);
60+
CleanupIfNeeded();
61+
return removed;
62+
}
63+
64+
private void CleanupIfNeeded()
65+
{
66+
if (DateTime.UtcNow - _lastCleanupTime > _cleanupFrequency)
67+
{
68+
foreach (var key in _cache.Keys)
69+
{
70+
if (DateTime.UtcNow > _cache[key].ExpirationTime)
71+
{
72+
_cache.TryRemove(key, out _);
73+
}
74+
}
75+
_lastCleanupTime = DateTime.UtcNow;
76+
}
77+
}
78+
}
79+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
using System.Linq.Dynamic.Core.Parser;
2+
using System.Threading.Tasks;
3+
using Xunit;
4+
5+
namespace System.Linq.Dynamic.Core.Tests
6+
{
7+
public partial class EntitiesTests
8+
{
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+
if (!expressions.TryGetValue(2000, out _))
20+
{
21+
Assert.Fail("Cache was missing constant expression for 2000");
22+
}
23+
24+
// Wait for the slide cache to expire, check on second later
25+
await Task.Delay(ConstantExpressionHelper.Expressions.TimeToLive.Add(TimeSpan.FromSeconds(1)));
26+
27+
if (expressions.TryGetValue(2000, out _))
28+
{
29+
Assert.Fail("Expected constant to be expired 2000");
30+
}
31+
}
32+
}
33+
}

0 commit comments

Comments
 (0)