Skip to content

Fix: Add Fallback in ExpressionPromoter to Handle Cache Cleanup in ConstantExpressionHelper #905

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

Open
wants to merge 10 commits into
base: master
Choose a base branch
from

Conversation

RenanCarlosPereira
Copy link
Contributor

🧩 Problem Summary

In multi-threaded scenarios, a race condition can occur due to the internal behavior of SlidingCache used in ConstantExpressionHelper. Specifically:

  • When a constant is added via CreateLiteral(value, text), both _expressions and _literals caches are populated.
  • Later, if _literals is cleaned up (due to TTL or cleanup frequency), ExpressionPromoter.Promote() will fail to retrieve the text representation of the constant.
  • As a result, numeric promotion fails, causing runtime exceptions like:
Operator '<' incompatible with operand types 'Decimal' and 'Double'

This issue was especially noticeable when comparing expressions like:

(x.A / x.B) < 0.40

✅ What This PR Does

This PR adds a safe fallback mechanism in ExpressionPromoter.Promote():

  • If _constantExpressionHelper.TryGetText(...) fails (because _literals was cleaned),
  • We now fallback to ce.Value?.ToString() to obtain a text representation for parsing.
  • Type promotion then continues as expected using the parsed value.

This ensures the parser remains robust and consistent, even when cache entries are removed in the background.

🧪 How We Reproduced the Issue

A unit test was created to simulate this scenario by:

  1. Adding a constant via CreateLiteral(...).
  2. Manually removing the corresponding _literals entry.
  3. Calling Promote() after cleanup.
  4. Verifying that promotion fails without a fallback — and succeeds after this fix.

📈 Impact

  • Fixes an intermittent failure that can happen in real-world concurrent scenarios.
  • Makes ExpressionPromoter safer and more fault-tolerant.
  • Enables continued parsing even if internal caches are cleaned up.

🚀 Ready for Review

Let me know if there are preferences for making the fallback smarter (e.g. re-adding the literal to cache or applying type suffixes). This version keeps it simple and safe.

@RenanCarlosPereira RenanCarlosPereira marked this pull request as draft March 24, 2025 10:49
@RenanCarlosPereira RenanCarlosPereira marked this pull request as ready for review March 24, 2025 10:50
Copy link
Collaborator

@StefH StefH left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. When I just copy this test to the master branch, it does not fail, can you double check if this test actually tests this exact scenario?

  2. The fix is limited to 1 place, but the ConstantExpressionHelper is used in multiple places.
    I was just wondering, would setting ReturnExpiredItems to true also solve this issue in all places?

@RenanCarlosPereira
Copy link
Contributor Author

RenanCarlosPereira commented Apr 7, 2025

Oh, you are right. When running the tests all together, it didn't fail, but when running only once, it failed. Here is why:

The issue is that ConstantExpressionHelperFactory uses a static instance (_instance) to cache ConstantExpressionHelper. This means all tests share the same instance, even if they use different ParsingConfig settings.

Since the _instance is static, it doesn't reset between tests, causing conflicts when tests try to use different configurations (e.g., CleanupFrequency, TimeToLive).

To make tests isolated, we use reflection to overwrite _instance before the test:

var field = typeof(ConstantExpressionHelperFactory)
    .GetField("_instance", BindingFlags.NonPublic | BindingFlags.Static);
field?.SetValue(null, new ConstantExpressionHelper(parsingConfig));

This ensures each test has its own independent ConstantExpressionHelper with the desired configuration.

About the ReturnExpiredItems: it won't work to set that to true because the Cleanup method does not take that flag into consideration.

@RenanCarlosPereira RenanCarlosPereira requested a review from StefH April 7, 2025 11:23
@RenanCarlosPereira
Copy link
Contributor Author

any updates on this? thanks

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

3 participants