Skip to content

Design doc for batch and priority-ordered CheckRules - #4978

Open
rockfordlhotka wants to merge 4 commits into
mainfrom
issue-3736-4235/plan
Open

rockfordlhotka wants to merge 4 commits into
mainfrom
issue-3736-4235/plan

Conversation

@rockfordlhotka

@rockfordlhotka rockfordlhotka commented Nov 1, 2026 •

Copy link
Copy Markdown
Member

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

Review feedback addressed here: the pass-2 set notation, and per-runner options (both from Stefan Ossendorf).

🤖 Generated with Claude Code

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 StefanOssendorf left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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**

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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. 🤔

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

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, temporary
  • MaxCheckRulesPasses became BatchCheckRulesOptions.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).

rockfordlhotka and others added 2 commits November 5, 2026 18:26
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>
@rockfordlhotka rockfordlhotka changed the title Plan for batch and priority-ordered CheckRules Design doc for batch and priority-ordered CheckRules Nov 6, 2026
@rockfordlhotka
rockfordlhotka marked this pull request as ready for review November 6, 2026 00:32

This branch has not been deployed

No deployments
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.

2 participants