Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions src/RulesEngine/RulesCache.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) Microsoft Corporation.
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using RulesEngine.HelperFunctions;
Expand Down Expand Up @@ -41,6 +41,12 @@ public Func<object[], Dictionary<string, object>> GetGlobalParamsDelegate(string
return _compiledGlobalParams.Get<Func<object[], Dictionary<string, object>>>(compiledRuleKey);
}

/// <summary>Removes any cached workflow-level global-params delegate for the specified compiled rule key.</summary>
public void RemoveGlobalParamsDelegate(string compiledRuleKey)
{
_compiledGlobalParams.Remove(compiledRuleKey);
}


/// <summary>Determines whether [contains workflow rules] [the specified workflow name].</summary>
/// <param name="workflowName">Name of the workflow.</param>
Expand Down Expand Up @@ -150,7 +156,7 @@ public void Remove(string workflowName)
{
if (_workflow.TryRemove(workflowName, out var workflowObj))
{
var compiledKeysToRemove = _compileRules.GetKeys().Where(key => key.StartsWith(workflowName)).ToList();
var compiledKeysToRemove = _compileRules.GetKeys().Where(key => key.StartsWith($"{workflowName}-", StringComparison.Ordinal)).ToList();
foreach (var key in compiledKeysToRemove)
{
_compileRules.Remove(key);
Expand Down
10 changes: 7 additions & 3 deletions src/RulesEngine/RulesEngine.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) Microsoft Corporation.
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using FluentValidation;
Expand Down Expand Up @@ -157,7 +157,7 @@ public async ValueTask<ActionRuleResult> ExecuteActionWorkflowAsync(string workf
sortedRuleParams.Sort((a, b) => string.Compare(a.Name, b.Name));
var sortedArr = sortedRuleParams.ToArray();

// Compile the whole workflow once and reuse — was previously recompiled every call,
// Compile the whole workflow once and reuse � was previously recompiled every call,
// which was the hot path the reporter of #471 saw.
if (!RegisterRule(workflowName, sortedArr))
{
Expand Down Expand Up @@ -422,13 +422,17 @@ private bool RegisterRule(string workflowName, params RuleParameter[] ruleParams
var globalParamsDelegate = _ruleCompiler.CompileScopedParams(workflow.RuleExpressionType, ruleParams, globalParamValues);
_rulesCache.AddOrUpdateGlobalParamsDelegate(compileRulesKey, globalParamsDelegate);
}
else
{
_rulesCache.RemoveGlobalParamsDelegate(compileRulesKey);
}

var enabledRules = workflow.Rules.Where(c => c.Enabled).ToArray();
var compiledFuncs = new RuleFunc<RuleResultTree>[enabledRules.Length];

// Parallel compilation helps only when:
// - the user opted in,
// - they're not also on UseFastExpressionCompiler (which regresses ~3× under
// - they're not also on UseFastExpressionCompiler (which regresses ~3� under
// parallel contention; FEC's internal locking serializes effort), and
// - there are enough rules to amortize Parallel.For's scheduling cost.
var shouldParallelize = _reSettings.EnableParallelRuleCompilation
Expand Down
82 changes: 82 additions & 0 deletions test/RulesEngine.UnitTest/CacheRemoveAndStaleGlobalParamsTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using RulesEngine.Models;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Threading.Tasks;
using Xunit;

namespace RulesEngine.UnitTest
{
[Trait("Category", "Unit")]
[ExcludeFromCodeCoverage]
public class CacheRemoveAndStaleGlobalParamsTest
{
[Fact]
public async Task AddOrUpdateWorkflow_RemovingGlobalParams_ClearsStaleGlobalParamsDelegate()
{
var workflowV1 = new Workflow
{
WorkflowName = "DynamicGlobalsWorkflow",
GlobalParams = new List<ScopedParam>
{
new ScopedParam { Name = "bonus", Expression = "input1.Value * 2" }
},
Rules = new List<Rule>
{
new Rule { RuleName = "Rule1", Expression = "bonus > 10" }
}
};

var engine = new RulesEngine(new[] { workflowV1 });

var resV1 = await engine.ExecuteAllRulesAsync("DynamicGlobalsWorkflow",
new RuleParameter("input1", new { Value = 10 }));
Assert.True(resV1[0].IsSuccess);
Assert.True(resV1[0].Inputs.ContainsKey("bonus"));

// Update workflow to remove GlobalParams completely.
var workflowV2 = new Workflow
{
WorkflowName = "DynamicGlobalsWorkflow",
GlobalParams = null,
Rules = new List<Rule>
{
new Rule { RuleName = "Rule1", Expression = "input1.Value > 5" }
}
};

engine.AddOrUpdateWorkflow(workflowV2);

var resV2 = await engine.ExecuteAllRulesAsync("DynamicGlobalsWorkflow",
new RuleParameter("input1", new { Value = 10 }));
Assert.True(resV2[0].IsSuccess);
Assert.False(resV2[0].Inputs.ContainsKey("bonus"));
}

[Fact]
public void RulesCache_RemoveWorkflow_DoesNotEvictOtherWorkflowWithSameNamePrefix()
{
var cache = new RulesCache(new ReSettings());
var wfOrder = new Workflow { WorkflowName = "Order" };
var wfOrderApproval = new Workflow { WorkflowName = "OrderApproval" };

cache.AddOrUpdateWorkflows("Order", wfOrder);
cache.AddOrUpdateWorkflows("OrderApproval", wfOrderApproval);

var dummyRules = new Dictionary<string, RuleFunc<RuleResultTree>>();
cache.AddOrUpdateCompiledRule("Order-input1_Object", dummyRules);
cache.AddOrUpdateCompiledRule("OrderApproval-input1_Object", dummyRules);

Assert.True(cache.AreCompiledRulesUpToDate("Order-input1_Object", "Order"));
Assert.True(cache.AreCompiledRulesUpToDate("OrderApproval-input1_Object", "OrderApproval"));

cache.Remove("Order");

Assert.False(cache.AreCompiledRulesUpToDate("Order-input1_Object", "Order"));
// "OrderApproval" must still have its compiled rules cached and up-to-date.
Assert.True(cache.AreCompiledRulesUpToDate("OrderApproval-input1_Object", "OrderApproval"));
}
}
}