Table of Contents

Class MotivLimits

Namespace
Motiv
Assembly
Motiv.dll

Process-wide limits on how much work a single evaluation may do. Set them once at startup, as with ExplanationDetail.

public static class MotivLimits
Inheritance
MotivLimits
Inherited Members

Fields

DefaultMaxEvaluationSize

The default value of MaxEvaluationSize.

public const int DefaultMaxEvaluationSize = 250000

Field Value

int

Remarks

Derived rather than chosen: a node of the thinnest composition there is costs about 190 bytes of retained result (measured over left-deep And chains of 1,000 to 100,000 propositions), so a quarter of a million of them puts a single evaluation's ceiling near 50 MB. That is far above any composition an author writes — 250,000 nodes is a chain of 125,000 propositions — and far below what a request body should be able to spend.

Properties

MaxEvaluationSize

The maximum number of nodes a single evaluation may compose before it is abandoned with a SpecException — one node per proposition evaluated, and one per logical operation joining them, so a chain of n propositions is 2n - 1 nodes.

public static int MaxEvaluationSize { get; set; }

Property Value

int

Remarks

Applies to Evaluate(TModel) and Matches(TModel) alike — Matches materialises no results, but it walks the same tree, and a composition one entry point accepts should never be one the other refuses.

This is a backstop in the engine, not a validator. A host that binds rule documents should refuse an oversized document at its edge — RuleSerializerOptions.MaxCompositionDepth does that, and its message can name the document where this one can only name a count.

It counts the nodes of the logical composition the evaluation folds, which is the quantity a flat operand array controls. Work done inside a node — a higher-order proposition over a large collection, say — is not counted and is not bounded by this.

That exclusion is declared rather than detected, and the distinction matters when you write the node. The engine cannot tell a re-entrant evaluation that is part of the composition from one that is work inside a node, so the library marks the places it knows: reaching a higher-order proposition's decision — resolving its elements and applying the predicate to them, including one you supplied through As(...)EnumerableExtensions.Where, a Tap callback, everything Motiv's own telemetry does with a span, tagging it with the result's explanation and running the listener callbacks Activity.Dispose() fires, and the WhenTrue/WhenFalse and cause-selecting delegates a higher-order result resolves when one of its properties is read. Everything else that evaluates a proposition while an evaluation is in flight is counted — notably a predicate of your own that evaluates a proposition per item (Spec.Build((Order o) => o.Lines.All(line.Matches))). Prefer the built-in quantifiers — AsAllSatisfied and its siblings — where the per-item work should not count against the rule that contains it.

The As(...) predicate joined the excluded side in #208. It is how the node reaches its own answer — a quorum whose threshold is itself a proposition is one evaluation inside one node, not part of the composition the node sits in — and until then the elements were excluded while the answer they were resolved for was not, which is not a line anyone could have described.

A higher-order result's own delegates joined the excluded side in #213, and the reason is that they run after Satisfied is fixed: the result is handed its outcome when it is constructed, so a cause selector or a WhenTrue read off it describes a decision rather than reaching one. That is what separates them from the per-item predicate above, which is how its node reaches an answer. Charged, they were charged to whichever evaluation was running at the moment of the first read — and these properties are memoized, so that was a fact about who read them first rather than about the composition. Merely attaching a telemetry listener warmed the memo and made a later refusal disappear, which is #209's rule with the sign reversed.

Telemetry is on the excluded side deliberately, and it is the one entry a caller does not write: attaching a listener must not change what an evaluation decides. Charged, merely subscribing to the Motiv source could push a composition past this limit — and, for an evaluation nested inside a running one, do it at an unrelated node (#209).

Work spread across decorator layers is counted, though. A decorator between two operator layers is not folded — it re-enters the fold — but the nested fold spends the same budget, so fifty layers of ten operands is refused by the same limit of 100 that refuses the flat chain of 200. It was not, until #202: the count lived in a fold-local, and the shape a rule document composes is exactly the alternating one.

EvaluateAsync(TModel, CancellationToken) and MatchesAsync(TModel, CancellationToken) count the same way, which they did not until #204: they held a fold-local count of their own, because the budget was a thread-static — correct for the synchronous folds, which never leave the thread that started them, and wrong for an asynchronous one, whose continuation may resume on a thread whose slot holds a suspended evaluation's count. The count flows with the evaluation instead, which settles the two shapes the synchronous surface does not have: a concurrent operator's two branches count against one budget, and a synchronous proposition reached through ToAsyncSpec() counts against the asynchronous evaluation containing it rather than starting a second one.

Exceptions

ArgumentOutOfRangeException

The value is less than one.