Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce Sliding Cache to Constant Expression Helper #765

Merged
merged 11 commits into from
Jan 22, 2024
28 changes: 18 additions & 10 deletions src/System.Linq.Dynamic.Core/Parser/ConstantExpressionHelper.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
using System.Collections.Concurrent;
using System.Linq.Expressions;
using System.Linq.Expressions;

namespace System.Linq.Dynamic.Core.Parser
{
internal static class ConstantExpressionHelper
{
private static readonly ConcurrentDictionary<object, Expression> Expressions = new();
private static readonly ConcurrentDictionary<Expression, string> Literals = new();
#if DEBUG
private static readonly TimeSpan TimeToLivePeriod = TimeSpan.FromSeconds(10);
#else
private static readonly TimeSpan TimeToLivePeriod = TimeSpan.FromMinutes(10);
#endif

public static readonly ThreadSafeSlidingCache<object, Expression> Expressions = new(TimeToLivePeriod);
private static readonly ThreadSafeSlidingCache<Expression, string> Literals = new(TimeToLivePeriod);


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

public static Expression CreateLiteral(object value, string text)
{
if (!Expressions.ContainsKey(value))
if (Expressions.TryGetValue(value, out var outputValue))
{
ConstantExpression constantExpression = Expression.Constant(value);

Expressions.TryAdd(value, constantExpression);
Literals.TryAdd(constantExpression, text);
return outputValue;
}

return Expressions[value];
ConstantExpression constantExpression = Expression.Constant(value);

Expressions.AddOrUpdate(value, constantExpression);
Literals.AddOrUpdate(constantExpression, text);

return constantExpression;
}
}
}
80 changes: 80 additions & 0 deletions src/System.Linq.Dynamic.Core/Parser/ThreadSafeSlidingCache.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
using System.Collections.Concurrent;

namespace System.Linq.Dynamic.Core.Parser
{
internal class ThreadSafeSlidingCache<T1, T2> where T1 : notnull where T2 : notnull
{
private readonly ConcurrentDictionary<T1, (T2 Value, DateTime ExpirationTime)> _cache;
private readonly TimeSpan _timeToLive;
private readonly TimeSpan _cleanupFrequency;
private DateTime _lastCleanupTime = DateTime.MinValue;

public ThreadSafeSlidingCache(TimeSpan timeToLive, TimeSpan? cleanupFrequency = null)
{
_cache = new ConcurrentDictionary<T1, (T2, DateTime)>();
_timeToLive = timeToLive;
_cleanupFrequency = cleanupFrequency ?? TimeSpan.FromSeconds(10);
}

public TimeSpan TimeToLive => _timeToLive;

public void AddOrUpdate(T1 key, T2 value)
{
if (key == null) throw new ArgumentNullException(nameof(key));
if (value == null) throw new ArgumentNullException(nameof(value));

var expirationTime = DateTime.UtcNow.Add(_timeToLive);
_cache[key] = (value, expirationTime);

CleanupIfNeeded();
}

public bool TryGetValue(T1 key, out T2 value)
{
if (key == null) throw new ArgumentNullException(nameof(key));

CleanupIfNeeded();

if (_cache.TryGetValue(key, out var valueAndExpiration))
{
if (DateTime.UtcNow <= valueAndExpiration.ExpirationTime)
{
value = valueAndExpiration.Value;
_cache[key] = (value, DateTime.UtcNow.Add(_timeToLive));
return true;
}
else
{
// Remove expired item
_cache.TryRemove(key, out _);
}
}

value = default!;
return false;
}

public bool Remove(T1 key)
{
if (key == null) throw new ArgumentNullException(nameof(key));
var removed = _cache.TryRemove(key, out _);
CleanupIfNeeded();
return removed;
}

private void CleanupIfNeeded()
{
if (DateTime.UtcNow - _lastCleanupTime > _cleanupFrequency)
{
foreach (var key in _cache.Keys)
{
if (DateTime.UtcNow > _cache[key].ExpirationTime)
{
_cache.TryRemove(key, out _);
}
}
_lastCleanupTime = DateTime.UtcNow;
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
using System.Linq.Dynamic.Core.Parser;
using System.Threading.Tasks;
using Xunit;

namespace System.Linq.Dynamic.Core.Tests
{
public partial class EntitiesTests
{
[Fact]
public async Task TestConstantExpressionLeak()
{
//Arrange
PopulateTestData(1, 0);

var populateExpression = _context.Blogs.All("BlogId > 2000");

var expressions = ConstantExpressionHelper.Expressions;

// Should contain
if (!expressions.TryGetValue(2000, out _))
{
Assert.Fail("Cache was missing constant expression for 2000");
}

// wait half the expiry time
await Task.Delay(TimeSpan.FromSeconds(ConstantExpressionHelper.Expressions.TimeToLive.TotalSeconds/2));
if (!expressions.TryGetValue(2000, out _))
{
Assert.Fail("Cache was missing constant expression for 2000 (1)");
}

// wait another half the expiry time, plus one second
await Task.Delay(TimeSpan.FromSeconds((ConstantExpressionHelper.Expressions.TimeToLive.TotalSeconds / 2)+1));
if (!expressions.TryGetValue(2000, out _))
{
Assert.Fail("Cache was missing constant expression for 2000 (2)");
}

// Wait for the slide cache to expire, check on second later
await Task.Delay(ConstantExpressionHelper.Expressions.TimeToLive.Add(TimeSpan.FromSeconds(1)));

if (expressions.TryGetValue(2000, out _))
{
Assert.Fail("Expected constant to be expired 2000");
}
}
}
}