Repository navigation
Description
Issue Summary
We encountered a race condition in ConstantExpressionHelper that can lead to parsing failures in ExpressionPromoter when _literals is cleaned up.
This issue is caused by SlidingCache, which automatically removes expired items, making Promote() unable to retrieve the required text representation of constants.
How This Issue Occurs
-
A thread calls
CreateLiteral(value, text), storing:_expressions.AddOrUpdate(value, constantExpression);_literals.AddOrUpdate(constantExpression, text);
-
Another thread runs cache cleanup, removing
_literalsand_expressionsdue to theCleanupFrequencyandTimeToLiveproperties inConstantExpressionCacheConfig. -
A third thread calls
Promote(), which attempts to retrieve the text using:_constantExpressionHelper.TryGetText(ce, out var text);
- Since
_literalswas cleaned,TryGetText()returns false, andPromote()fails to perform the type promotion.
- Since
-
This leads to an error when trying to compare a
doublewith adecimal, causing:Exception while parsing expression `Variable < 0.40` - Operator '<' incompatible with operand types 'Decimal?' and 'Double'
Reproduction
We created a unit test that simulates this race condition:
-
Test Name:
Promote_Should_Succeed_Even_When_LiteralsCache_Is_Cleaned -
Steps:
- Add a constant to the cache.
- Simulate cache cleanup by repeatedly calling
TryGetText()until_literalsis cleaned. - Attempt to
Promote()the expression after cleanup. - Expected:
Promote()should still return a valid expression (not null).
-
Without Fix: The test fails due to missing
_literalsentries. -
With Fix:
Promote()properly handles the absence of_literals.
Proposed Fix
-
Ensure
Promote()does not depend entirely on_literals:- If
TryGetText()fails, fallback to_expressionsor recompute the value.
- If
-
Prevent aggressive cleanup of
_literals:- Increase
_literalsTTL. - Keep
_expressionsand_literalsin sync.
- Increase
-
Lazy loading mechanism for
_literals:- If
_literalsis empty, reconstruct the text from_expressions.
- If
Next Steps
- Review and confirm the issue using the provided test.
- Discuss the best approach for fixing this issue.
- Implement the fix and verify with tests.
[Fact]
public async Task Promote_Should_Succeed_Even_When_LiteralsCache_Is_Cleaned()
{
// Arrange
var parsingConfig = new ParsingConfig()
{
ConstantExpressionCacheConfig = new Core.Util.Cache.CacheConfig
{
CleanupFrequency = TimeSpan.FromMilliseconds(500), // Run cleanup more often
TimeToLive = TimeSpan.FromMilliseconds(500), // Shorten TTL to force expiration
ReturnExpiredItems = false
}
};
var constantExpressionHelper = ConstantExpressionHelperFactory.GetInstance(parsingConfig);
var expressionPromoter = new ExpressionPromoter(parsingConfig);
double value = 0.40;
string text = "0.40";
Type targetType = typeof(decimal);
// Step 1: Add constant to cache
var literalExpression = constantExpressionHelper.CreateLiteral(value, text);
Assert.NotNull(literalExpression); // Ensure it was added
// Step 2: Manually trigger cleanup
var cts = new CancellationTokenSource(500);
await Task.Run(async () =>
{
while (!cts.IsCancellationRequested)
{
constantExpressionHelper.TryGetText(literalExpression, out _);
await Task.Delay(50); // Give some time for cleanup to be triggered
}
});
// Ensure some cleanup cycles have passed
await Task.Delay(500); // Allow cache cleanup to happen
// Step 3: Attempt to promote the expression after cleanup
var promotedExpression = expressionPromoter.Promote(literalExpression, targetType, exact: false, convertExpression: true);
// Assert: Promotion should still work even if the cache was cleaned
promotedExpression.Should().NotBeNull(); // Ensure `Promote()` still returns a valid expression
}
Would love feedback on the best approach! 🚀
Activity
RenanCarlosPereira commentedon Mar 20, 2025
Note: this is an example where this could go wrong between
expression1andexpression2, if time passes and other threads are happening, and the cache is cleaned.RenanCarlosPereira commentedon Mar 21, 2025
it might be related to this one #769
RenanCarlosPereira commentedon Mar 23, 2025
Opened the pull request #905 please take a look 👍
grichards23 commentedon Mar 28, 2025
I definitely feel like we're running into this in our implementation. We've been chasing for a couple of hours.
Eager to try to apply this fix and see if helps, any idea when the PR will be approved and a patch version will be out?
Thank you!
RenanCarlosPereira commentedon Apr 7, 2025
One thing that you could try to do is set the
CleanupFrequencyandTimeToLivein theConstantExpressionCacheConfigfor, let's say 10 years, that will solve the problem.but the dictionary will never be clear, so depending on how much constants you have in your expressions, you will be seeing a memory leak
RenanCarlosPereira commentedon May 28, 2025
@StefH would you mind creating a version?