Skip to content

Route ExecuteActionWorkflowAsync through RulesCache (fixes #471)#734

Merged
YogeshPraj merged 2 commits into
microsoft:mainfrom
YogeshPraj:fix/issues-batch-3
May 29, 2026
Merged

Route ExecuteActionWorkflowAsync through RulesCache (fixes #471)#734
YogeshPraj merged 2 commits into
microsoft:mainfrom
YogeshPraj:fix/issues-batch-3

Conversation

@YogeshPraj
Copy link
Copy Markdown
Contributor

Summary

Tier-3 perf follow-up. Two real outcomes plus a write-up for the third.

Fixes #471 — Rule-chaining (EvaluateRuleAction) recompiled the target rule on every link

ExecuteActionWorkflowAsync called a private CompileRule(workflowName, ruleName, ruleParameters) overload that always rebuilt the per-rule wrapper delegates from scratch — no RulesCache lookup. With EvaluateRuleAction this turned every link in a chain into a fresh compilation pass even when the workflow was already registered.

Routed ExecuteActionWorkflowAsync through the same RegisterRule + GetCompiledRules + ApplyGlobalParams path that ExecuteAllRulesAsync uses. First call still compiles; subsequent calls with the same workflow + param shape are cache hits. Globals are evaluated once via the workflow-level cached delegate (consistent with the #714 model).

Side cleanups:

Benchmark (median of 5 runs, .NET 8, 10-deep rule chain via EvaluateRuleAction)

Scenario Before (master) After (this PR) Speedup
Chain × 100 invocations 7 ms 3 ms ~2.3×
Chain × 1,000 invocations 65 ms 42 ms ~1.5×

The remaining time is delegate invocation + ActionContext JSON-serialization. Reworking the latter would be the next obvious win but is out of scope here.

Addresses #513 — "13 rules × 600 records = 13 seconds" with a documented pattern

The original 13-second number was almost certainly the v5.0.1 cache regression (fixed in #727). On 6.0.1-preview.1 the same scenario completes in ~5 ms for the no-short-circuit case and ~2 ms with the recommended pattern. No code change needed.

Added Issue513Test that documents the recommended OR-short-circuit pattern:

  • Wrap the N rules under a single parent rule with Operator = "OrElse".
  • Set ReSettings.NestedRuleExecutionMode = NestedRuleExecutionMode.Performance.
  • The engine evaluates children left-to-right and stops at the first true.

The test asserts the short-circuit fires (ChildResults.Count == 1 when the first child matches) and the no-short-circuit baseline still runs all 13.

Defers #707 — vague repro, no fix in this PR

The reporter said "after upgrading to 6.02 … much slower performance … a lot of first chance exceptions thrown, dynamic assembly types I think." No code, no benchmark numbers, no version string we can pin down (6.02 is ambiguous).

I'd recommend closing this in favor of asking the reporter for:

Happy to do that as a comment on the issue rather than in this PR.

Test plan

  • Two new test files (Issue471Test.cs, Issue513Test.cs) — 5 new tests.
  • All 160 unit tests pass on net6.0 / net8.0 / net9.0 / net10.0.
  • Existing RulesEngineWithActionsTests (which exercises ExecuteActionWorkflowAsync and EvaluateRuleAction) still passes — confirms the cache-routing refactor preserves behavior.
  • No public API changes; behavior changes only on the hot path.

)

ExecuteActionWorkflowAsync previously called a private CompileRule(workflowName,
ruleName, ...) overload that always rebuilt the per-rule wrapper delegates from
scratch — no RulesCache lookup. With EvaluateRuleAction this turned every link
in a rule chain into a fresh compilation pass even when the workflow was
already registered.

This change routes the method through the same RegisterRule + GetCompiledRules
+ ApplyGlobalParams path that ExecuteAllRulesAsync uses. First call still
compiles; subsequent calls with the same workflow + param shape are cache hits.
Globals are evaluated once via the workflow-level cached delegate, matching
the existing microsoft#714 behavior.

Side cleanups:
- The now-unused private CompileRule(workflowName, ruleName, ruleParameters)
  overload is removed.
- EvaluateGlobalsAdHoc and CompileGlobalParamsDelegate are gone — the cache
  path supersedes them.

Benchmark (median of 5 runs, net8.0, 10-deep rule chain via EvaluateRuleAction):
  Chain x 100 invocations:     7 ms -> 3 ms   (~2.3x)
  Chain x 1,000 invocations:  65 ms -> 42 ms  (~1.5x)

The remaining time is delegate invocation + ActionContext setup; further wins
would need ActionContext JSON-serialization to be reworked, which is out of
scope here.

Also adds regression tests for both microsoft#471 and microsoft#513:
- Issue471Test verifies the cached path, chain correctness, and per-workflow
  cache-key isolation.
- Issue513Test documents the recommended OR-short-circuit pattern
  (NestedRuleExecutionMode.Performance under a single OrElse parent rule) for
  workflows that want "include if any rule matches" semantics.

All 160 unit tests pass on net6 / net8 / net9 / net10.
@YogeshPraj YogeshPraj merged commit 3e6ce8b into microsoft:main May 29, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Performance issue with rule-chaining

2 participants