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