Dmn.Limits (dmn v0.1.0)

Copy Markdown View Source

The bounds that dmn keeps on a model that comes from outside.

A model is untrusted input, and so are the models it imports. Passing a bound is an error value of the kind :limit (Dmn.Error), never a crash. So is a limits: option that is none of nil, a t/0 or a keyword list of its fields (validate/1, DD-39). The defaults are generous for a model that a person wrote (the largest in the TCK is a few hundred kB) and small for one made to hurt; a caller that reads models it trusts can raise them, and one that reads models of anyone should lower them to what its models need.

Reading the XML (Dmn.XML)

FieldDefaultBounds
max_bytes5,000,000the size of one document
max_depth100how deep the elements nest
max_elements200,000the elements in one document
max_attributes100the attributes of one element, and its namespace declarations
max_name_length256the length of the name of an element or an attribute, and of a prefix
max_uri_length1,024the length of a namespace URI; every element that is in it holds a copy of it
xml_timeout5,000milliseconds that reading one document may take (Feel.Worker)
max_memory1,000,000,000bytes that the process reading it may grow to

The SAX parser takes time quadratic in the attributes of one element, and has no bound of its own. The count is checked when the parser reports the element, which is after the work; xml_timeout and max_memory are what stop a document made to waste it.

These document limits do not widen an enclosing guard: policy. Ordinary Dmn.load/2 and Dmn.compile/2 default to a 128 MiB heap and a five-second deadline for the complete operation, including its input/result transfers. The XML worker inherits tighter enclosing allowances.

Imports (Dmn.Resolver)

FieldDefaultBounds
max_units10,000input data, decisions, knowledge models and services in the whole graph
max_models64the models of one compile, the root and all it imports
max_import_depth16the imports in a chain
max_import_reads256the documents that the resolver is asked for, whatever comes of them (an import that fails is a read too)
max_import_bytes20,000,000the bytes of all the documents that the imports bring, the ones that cannot be read included
import_timeout30,000milliseconds that reading all the imports may take; a document is read with what is left of it as its xml_timeout

The checks of the decision tables (Dmn.Compiler.Checks)

FieldDefaultBounds
table_check_timeout1,000milliseconds that the questions about one table (overlap, never reached, complete) may take
checks_timeout10,000milliseconds that the questions about all the tables of one compile may take

The number of rules and of cells of a table that is asked about is limited (200 and 4,000), and the time is not bounded by that: 100 rules of ranges and not(…) with 4 inputs took 38 s to answer and 6 ms to compile without. The questions are asked in a process that is ended at the deadline, and a table that passed it is said to be unchecked (:table_not_checked, an info), which is not an answer to the questions.

The limits of one document, times the models, are more than a compile should spend (max_models times max_bytes is 320 MB of text), and a root can hold tens of thousands of imports that all fail: so the whole graph of imports has a budget of its own. A document is asked for once for each model and location (Dmn.Imports), so the same import that fails twice is not read twice. The resolver itself is the caller's, and its time is only counted, not cut: guard: on Dmn.compile/2 is what ends it.

Summary

Functions

Returns the limits that Dmn functions take as an option: a t/0, or the fields to replace. Raises as new/1 does; validate/1 does not.

The default limits, with any field replaced.

The limits that a caller passed as an option (limits: of Dmn.load/2 and Dmn.compile/2), or why they are none: nil, a t/0 or a keyword list of its fields, with every value a positive integer. Nothing raises for what the caller passed (DD-39).

Types

t()

@type t() :: %Dmn.Limits{
  checks_timeout: pos_integer(),
  import_timeout: pos_integer(),
  max_attributes: pos_integer(),
  max_bytes: pos_integer(),
  max_depth: pos_integer(),
  max_elements: pos_integer(),
  max_import_bytes: pos_integer(),
  max_import_depth: pos_integer(),
  max_import_reads: pos_integer(),
  max_memory: pos_integer(),
  max_models: pos_integer(),
  max_name_length: pos_integer(),
  max_units: pos_integer(),
  max_uri_length: pos_integer(),
  table_check_timeout: pos_integer(),
  xml_timeout: pos_integer()
}

Functions

from_option(limits)

@spec from_option(t() | keyword() | nil) :: t()

Returns the limits that Dmn functions take as an option: a t/0, or the fields to replace. Raises as new/1 does; validate/1 does not.

new(fields \\ [])

@spec new(keyword()) :: t()

The default limits, with any field replaced.

iex> Dmn.Limits.new(max_depth: 10).max_depth
10

Raises ArgumentError for a field that is not one of the limits, or a value that is not a positive integer.

validate(limits)

@spec validate(term()) :: {:ok, t()} | {:error, String.t()}

The limits that a caller passed as an option (limits: of Dmn.load/2 and Dmn.compile/2), or why they are none: nil, a t/0 or a keyword list of its fields, with every value a positive integer. Nothing raises for what the caller passed (DD-39).

iex> {:ok, %Dmn.Limits{max_depth: 10}} = Dmn.Limits.validate(max_depth: 10)
iex> {:error, _message} = Dmn.Limits.validate(max_depth: -1)
iex> {:error, _message} = Dmn.Limits.validate(:x)