-
-
Notifications
You must be signed in to change notification settings - Fork 240
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! π