Repository navigation
Design doc for batch and priority-ordered CheckRules - #4978
rockfordlhotka wants to merge 4 commits into
Conversation
Design plan for reducing redundant rule reruns in CheckRules() (#3736) and running CheckRules() in rule priority order across properties (#4235), with an opt-out to the current behavior and room for future priority-ordered async rule execution. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fold the rule runner abstraction (suggested by Stefan Ossendorf) into the plan: runners decide which rules run and in what order, while BusinessRules keeps the single-rule mechanics behind a small host interface. Today's behavior moves to LegacyRuleRunner and the new batch behavior becomes BatchRuleRunner, selected through DI. The runner interfaces stay internal in v11, so RuleOptions.UseLegacyCheckRules remains the public opt-out. Also mark the baseline tests step as done (#4981) and renumber the PR sequence. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
StefanOssendorf
left a comment
There was a problem hiding this comment.
I'm not sure if this is the correct place:
Currently a BO is busy while it's rules are running. But the Rule completion is done after the busy state is set to non busy. At work that behavior results in a race condition. We are using WaitForIdle which triggers because IsBusy = false is set, but the async rule hasn't completed yet. But we go ahead and are done an dispose stuff. But the BO accesses the now disposed DI scope which results in exceptions. I managed that with a Settle method which listens in addition on the completed event handler. I'll post the method tomorrow when I'm at work.
Should we consider that in this rework or is this something independent?
| Short-circuit behavior stays per property, as today. Only the interleaving changes. | ||
| - Collect affected, output and dirty property names. | ||
| 3. **Pass 2: rerun only what dirty values invalidate.** With dirty set D, the groups to rerun are | ||
| `Q = D ∪ { r.PrimaryProperty | r.InputProperties ∩ D ≠ ∅ }`. Rerun each group in Q **in full** |
There was a problem hiding this comment.
This equation means D union Primary property. The intersection is irrelevant because it will be absorbed according to set operations.
I interpreted the | as an or.
There was a problem hiding this comment.
Good point, that notation was ambiguous. The | was meant as set-builder "such that", not "or". So the set is "the primary property of every rule whose input properties overlap D", and the intersection is the filter condition, not a set operation on the result.
I rewrote it in 94d2c9a. The doc now says it in words first: the properties in D, plus the primary property of every rule with at least one input in D. It then gives Q = D ∪ P, where P = { r.PrimaryProperty : r.InputProperties ∩ D ≠ ∅ }, with : for "such that".
For reference, this is BatchPlan.GroupsInvalidatedBy in #4990: for each dirty property, it adds that property's group and the groups of the rules that list it as an input.
| API, and a per-instance setting on `BusinessRules` would add a serialized field. | ||
| 2. **Object rules run first**, as today, and are not part of the priority interleave. Their dirty | ||
| outputs feed pass 2. | ||
| 3. **Configurable cycle cap.** Add `RuleOptions.MaxCheckRulesPasses`, the number of pass-2 rounds |
There was a problem hiding this comment.
Shouldn't we create runner dependent options? It's not very intuitive to have such a property but it's not respected by the configured runner. 🤔
There was a problem hiding this comment.
Agreed. A flat MaxCheckRulesPasses that the legacy runner silently ignores is confusing, and it would get worse once there's an async runner with options of its own.
#4990 now chooses the behavior with a method, and each method takes only that behavior's settings (fcc0dcd):
o.Rules(r => r.UseBatchCheckRules(b => b.MaxRerunPasses = 200)); // default; MaxRerunPasses defaults to 100
o.Rules(r => r.UseLegacyCheckRules()); // CSLA 10 behavior, temporaryMaxCheckRulesPassesbecameBatchCheckRulesOptions.MaxRerunPasses, so it's only reachable when you choose the batch behavior.- If both methods are called, the last call wins (covered by
RuleOptions_LastRunnerChoiceWins). - A future runner, such as priority-ordered async execution, adds its own
Use…(…)method with its own options. - Removing the legacy behavior later means deleting
UseLegacyCheckRules().
The runners themselves stay internal, so these methods are the public way to choose one. The plan doc's Decisions section is updated to match (94d2c9a).
Address review feedback on the plan: - Spell out the pass-2 rerun set in words before the set notation, and use ':' for "such that" so the set-builder bar isn't read as "or". - Rule options are now per behavior: UseBatchCheckRules(b => ...) with BatchCheckRulesOptions.MaxRerunPasses, and UseLegacyCheckRules(), so no setting is silently ignored by the chosen runner. Matches #4990. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Rewrite the plan as a record of the implementation in #4990: the actual IRuleRunner/IRuleRunnerHost interfaces and where the bookkeeping lives, the batch algorithm including the rerun handling of rules excluded in AsAffectedProperty mode, how the work was delivered (#4981 plus one commit per step in #4990), the measurements, why step 6 (skipping stale reruns) was measured but not merged, the final options design, what is and isn't in place for priority-ordered async execution, and the follow-ups (#4991, the legacy clear-then-skip cascade, CascadeIfDirty, the ignored CascadeTests). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Related to #3736 and #4235
This PR adds the design document for batch, priority-ordered
CheckRules()(docs/dev/CheckRules-Batch-Priority-Plan.md), with no code changes. It started as the plan, reviewed here before implementation. The work is now implemented in #4990 (on top of the baseline tests in #4981), and the doc has been updated to record the design as built, including where and why it differs from the original plan.What the doc covers
CheckRules().IRuleRunner(policy) is implemented byLegacyRuleRunner(the CSLA 10 behavior) andBatchRuleRunner(the new default).IRuleRunnerHost(mechanics) is implemented byBusinessRules.MaxRerunPasseswith cascading;Upgrading to CSLA 11.mdin Run CheckRules() as a priority-ordered batch through internal rule runners #4990.UseBatchCheckRules(b => b.MaxRerunPasses = …)/UseLegacyCheckRules()), object rules first, a cap that throws, v11 only, runners internal.WaitForIdle()returns before async rule completion finishes), the legacy clear-then-skip cascade, theCascadeIfDirtycheck, and the ignoredCascadeTests.Review feedback addressed here: the pass-2 set notation, and per-runner options (both from Stefan Ossendorf).
🤖 Generated with Claude Code