Interface RulesEngine<O>

Type Parameters:
O - The output object type to instantiate
All Superinterfaces:
AutoCloseable

public interface RulesEngine<O> extends AutoCloseable
Runs rules against facts: it evaluates each Rule's condition and fires the actions of the rules that match, as its match policy decides. Build one with RulesEngineBuilder, which sets its match policy, expression languages, imports, listeners, limit on compiled copies and run timeout once.

Lifecycle: compile the rules with load(List), then call run(FactStore) or runWithResult(FactStore) as often as needed, from any number of threads. Rules are evaluated in descending priority order; equal priorities keep their list order, and a null priority sorts last. load may be called again at any time to swap in new rules atomically. Close the engine with close() once it's no longer needed.

Implementing: you may implement this interface, for example to decorate an engine or as a test double. Implement load(List), validate(List), runWithResult(FactStore, RunOptions) and rules(); run(FactStore), runWithResult(FactStore) and close() have defaults. A method added in a later 2.x release is a default method, so an existing implementation keeps compiling. RunResult.of(Object, List, List, String) and RuleSetInfo.of(List, String, java.time.Instant) create the values an implementation returns.

See Also:
  • Method Summary

    Modifier and Type
    Method
    Description
    default void
    Closes the engine, and releases what its expression languages hold for the rules, such as interpreter contexts.
    void
    load(List<Rule> ruleList)
    Compiles a rule list and swaps it in for the rules loaded before, if any.
    Returns the rules the engine has loaded, their checksum and when they were loaded.
    default @Nullable O
    run(FactStore<?> facts)
    Runs the loaded rules against facts.
    default RunResult<O>
    Runs the rules like run(FactStore), and reports what the run did: the output object, the rules that fired, what each rule's condition evaluated to, the checksum of the rules the run used, and the run's tags and start instant.
    runWithResult(FactStore<?> facts, RunOptions options)
    Runs the rules like runWithResult(FactStore), with settings for this run only, such as a timeout that replaces the one the engine was built with.
    validate(List<Rule> ruleList)
    Compiles a rule list exactly as load(List) would, with this engine's languages, imports, options and declared facts, without loading it, and returns the problems load() would have thrown instead of throwing.
  • Method Details

    • load

      void load(List<Rule> ruleList)
      Compiles a rule list and swaps it in for the rules loaded before, if any.

      If closing an idle session or a compiler of the replaced rules throws with an InterruptedException in its cause chain, or suppressed on it, load() logs it at WARN, puts back the thread's interrupt status and still returns normally, unless it also carries a fatal Error, which is thrown as described below (see Session.close()).

      Parameters:
      ruleList - The list of Rule objects
      Throws:
      RuleCompilationException - if the list contains a null rule; two rules share a name; a condition or action is blank; a rule is written in a language the engine doesn't have; a condition or action has a syntax error its language detects, or is one its language rejects, such as a condition with an assignment; a language throws while creating its compiler, or returns null instead of a compiler or a compiled expression; a LinkageError such as a NoClassDefFoundError is thrown for a class a rule uses, which means the rule is misconfigured rather than the JVM failing; a compiler throws while telling which facts its rules read, or tells it with a set holding null (see ExpressionCompiler.factNamesRead()); or a declared fact has a name a language the rules use can't refer to, though a language whose compiler says which facts its rules read (ExpressionCompiler.factNamesRead(), as MVEL's does) checks only those, or a name that a language the rules use reserves only for the rule lists that use it, rejected whether or not a rule reads it (see ExpressionLanguage.reservesForEveryRuleList()). A null rule or a duplicate name is thrown at once. The other problems are collected before the exception is thrown, and failures() lists them: each broken rule, a language that can't create its compiler (once, in place of the first rule that needed it; the rules written in it aren't compiled and get no failure of their own), and each declared fact name the compilers that were created reject, or their languages reserve so. Once the rules compile, an engine built with copiesAtLoad(n) makes its copies of them, and a language that throws while creating or warming up a session for one, or returns null instead of a session, fails the load with this exception, naming the language, on its own.
      IllegalStateException - if the engine is closed; this is checked before anything in the list, so a closed engine throws it even for a list that would fail to load
      NullPointerException - if ruleList itself is null
      StackOverflowError - if the thread has too little stack left for the load's own steps, checked before it compiles anything; also if the list is the first to use a language of a class no engine has prepared yet, one the engine finds with ServiceLoader without its builder naming it, and the thread has too little stack left to prepare it (see ExpressionLanguage.prepare()): checked before the language is prepared, so nothing is left unusable, and the rules loaded before, if any, stay loaded. The room is about 160 KB on x64 while the check is interpreted, as in a JVM that has run few rules
      Error - a VirtualMachineError other than StackOverflowError, and other than an InternalError that wraps one with no LinkageError between them, as the JDK wraps an overflow deep in a stack, thrown while compiling or making copies, also as the cause of another exception or one suppressed on it, is logged and then rethrown unchanged. So is one a language throws while closing a session or a compiler, once everything being closed has been closed: the first, if there are several. When a load that failed closes what it made, that error is thrown in place of the load's own failure, unless the load failed with a fatal error, which came first and is thrown instead. The error thrown carries the others the load met, and the failure it replaces, as suppressed exceptions; if it can't carry one, as an OutOfMemoryError the JVM throws itself can't, that is logged at WARN. When a reload closes the rules it replaced, it is thrown after the new rules were swapped in: they stay loaded, and runs use them. The reload closes the idle copies of the rules it replaced, and closes their compilers too, throwing their error, if the last run using those rules left before it had finished closing the idle copies, as close() does. Once the new rules are swapped in, only a fatal error is thrown: anything else that stops closing old rules part way, as running out of stack in the engine's own steps can, is kept on that error as a suppressed exception, or logged at WARN if nothing was fatal, and the next load() or close() goes on closing those rules, throwing a fatal error from closing them as its own. A load that failed before the swap throws such a failure from closing its own rules in place of its own failure. A Throwable that is neither an Exception nor an Error is never fatal: it's handled like an exception from the same place, reported as a RuleCompilationException while compiling or making copies, and logged at WARN while closing.
    • validate

      List<RuleCompilationException> validate(List<Rule> ruleList)
      Compiles a rule list exactly as load(List) would, with this engine's languages, imports, options and declared facts, without loading it, and returns the problems load() would have thrown instead of throwing. The two lists aren't quite the same, and the paragraphs below say how. The rules loaded, if any, are unchanged, and the compiled result is discarded: a later load() compiles the list again. Nothing about the rules is logged, not even a language's compile warnings; a fatal Error is logged at ERROR before it's rethrown, and a compiler that fails to close at WARN, as always.

      One problem this method can't report: it makes no copies. An engine built with copiesAtLoad(n) above zero makes them once the rules compile, and a language that throws while creating or warming up a session for one, or returns null instead of a session, fails that load(), naming the language. With the default copiesAtLoad(0) nothing is outside what it can see.

      The problems come in the order load() finds them: a null entry or a duplicate name, in list order; then each rule that doesn't compile, in priority order, with a language that can't create its compiler in place of the first rule that needed it; then a compiler that fails to tell which facts its rules read; then each declared fact name the languages reject, or reserve only for the rule lists that use them. Unlike load(), a null entry or a duplicate name doesn't stop the check: every other rule is still compiled.

      Parameters:
      ruleList - The list of Rule objects
      Returns:
      One exception for each problem found, as load() would have reported it; empty when it finds none
      Throws:
      IllegalStateException - if the engine is closed
      NullPointerException - if the list itself is null
      StackOverflowError - as load(List) throws it: if the thread has too little stack left for the validation's own steps, checked before it compiles anything; also if the list is the first to use a language the engine wasn't built to use
      Error - a VirtualMachineError other than StackOverflowError, and other than an InternalError that wraps one with no LinkageError between them, as the JDK wraps an overflow deep in a stack, thrown while compiling, also as the cause of another exception or one suppressed on it, is logged and then rethrown unchanged. So is one a language throws while closing a compiler, once every compiler created has been closed: the first, if there are several, unless compiling threw a fatal error, which came first and is thrown instead. The error thrown carries the others as suppressed exceptions. Either way the problems found aren't returned.
    • run

      default @Nullable O run(FactStore<?> facts)
      Runs the loaded rules against facts.
      Parameters:
      facts - The facts to run the rules against. Any FactStore is accepted, such as a FactMap<Applicant>. The engine reads them through FactStore.asMap().
      Returns:
      The output of the actions of the matching Rule objects, or null if the rule list is empty or no rule matched
      Throws:
      RuleExecutionException - if a condition or action throws; a condition evaluates to null or a non-boolean; an action returns null instead of an ActionResult, or a property it returned can't be set on the output; the output supplier throws or returns null; a language throws or returns null when it creates a session for the run; or more than one rule matches on a unique-match engine, which names them all and belongs to no rule. Also if the run must stop, which is checked while it waits for a compiled copy of the rules, while it reads the engine's rules again after a reload or close() closed the list it had read, between rules and when each condition or action returns, because its thread was interrupted, which keeps the interrupt status set and makes the cause an InterruptedException, or because it passed the deadline a timeout gave it, which makes the cause a TimeoutException. Either belongs to no rule, so getRuleName() is null.
      IllegalArgumentException - if a fact's name is null, blank, or one of the engine's languages reserves (see ExpressionLanguage.reservedFactNames(), output by default; a language that reserves its names only for the rule lists that use it, as ExpressionLanguage.reservesForEveryRuleList() says, reserves them only when a loaded rule is written in it, or, when the loaded rule list has no rules, when it's the engine's default language), or a name that the language of a loaded rule can't refer to (a rule list without rules is checked against the engine's default language, and a language whose compiler says which facts its rules read checks only those, as ExpressionCompiler.factNamesRead() says); if a declared fact has a non-null value that isn't an instance of its declared type or, for a primitive type, of its wrapper or of a wrapper whose primitive Java widens to it; or, with RulesEngineBuilder.requireDeclaredFacts(), if a declared fact is missing or an undeclared one is supplied. Also if a language's check of a fact name fails with any other exception, which becomes the cause.
      IllegalStateException - if load(List) has not been called, or the engine is closed; or if the run, while borrowing a copy of the rules, found the same rule list closed each time it read the engine's rules again, many times in a row, which means an engine invariant has broken rather than that the call was wrong (a reload in between starts the count again, so reloads alone never cause this)
      NullPointerException - if facts is null
      StackOverflowError - if the thread has too little stack left for the run's own steps, checked before the run takes anything; more is checked for, after facts and the engine's state are, while a language class the rules use hasn't finished a run in the JVM, nested runs included, as a rule's first calls may make the JDK generate classes. It's thrown unchanged, leaving the engine as it was
      Error - a VirtualMachineError other than StackOverflowError, and other than an InternalError that wraps one with no LinkageError between them, as the JDK wraps an overflow deep in a stack, wherever it arises (a rule or Java code it calls, the output supplier, an output writer, a language checking a name, creating a session or closing one, or a listener), is rethrown unchanged, also when it arrives as the cause of another exception, or suppressed on it, as a try-with-resources whose close() fails while its body throws leaves it. A run that gives back an extra copy, made because every kept copy was in use, a copy that couldn't be kept for a later run, or a copy of rules a reload or close() replaced that no run of them is waiting for, closes that copy, and the last run to leave replaced rules closes the copies still kept, and then their compilers, unless the reload or close() is still closing the copies that were idle when it replaced them, which then closes the compilers itself once it has; a run whose new copy was only partly made closes the sessions it made; and a run that fails to get a copy does that closing without having held one, if it was the last to use the rules; if its wait for a copy, or for a build slot to make one, was stopped, it has already reported the stop to its listeners. A fatal error from that closing reaches the run: it's thrown even when the rules ran without failing, and in place of a failure of the run that isn't fatal, which it carries as a suppressed exception; if it can't carry one, as an OutOfMemoryError the JVM throws itself can't, that failure is logged at WARN. A fatal error of the run's own came first, and is thrown instead. Of several fatal errors, the first is thrown, carrying the others as suppressed exceptions; one a listener throws from onRunError is thrown in place of the run's failure, which it carries. Every other Error from a rule, the output supplier, an output writer or a language creating a session, including a LinkageError, is reported as a RuleExecutionException; one from a language's check of a fact name as an IllegalArgumentException; one from a listener is logged, and the run goes on; one from closing a session is logged at WARN. A Throwable that is neither an Exception nor an Error is never fatal: wherever it arises, it's handled like an exception from the same place.
    • runWithResult

      default RunResult<O> runWithResult(FactStore<?> facts)
      Runs the rules like run(FactStore), and reports what the run did: the output object, the rules that fired, what each rule's condition evaluated to, the checksum of the rules the run used, and the run's tags and start instant. A caller can record which rules produced a decision, and why the others didn't apply, without a RuleListener.
      Parameters:
      facts - The facts to run the rules against, as run(FactStore) takes them
      Returns:
      What the run did, never null. Its output is null exactly when no rule fired, because the rule list is empty or no condition matched, and its evaluations have one entry per loaded rule.
      Throws:
      RuleExecutionException - as run(FactStore) throws it
      IllegalArgumentException - as run(FactStore) throws it
      IllegalStateException - if load(List) has not been called, or the engine is closed; or if the run, while borrowing a copy of the rules, found the same rule list closed each time it read the engine's rules again, many times in a row, which means an engine invariant has broken rather than that the call was wrong (a reload in between starts the count again, so reloads alone never cause this)
      NullPointerException - if facts is null
      StackOverflowError - as run(FactStore) throws it
    • runWithResult

      RunResult<O> runWithResult(FactStore<?> facts, RunOptions options)
      Runs the rules like runWithResult(FactStore), with settings for this run only, such as a timeout that replaces the one the engine was built with.

      A timeout's deadline is taken from when this method is called, so waiting for a compiled copy of the rules counts towards it. The engine checks it while waiting, while the run reads the engine's rules again after a reload or close() closed the list it had read, and before and after each condition and each action, so a run stops between rules, and when the condition or action that was running returns; see RulesEngineBuilder.runTimeout(Duration) for what that does and doesn't stop.

      Parameters:
      facts - The facts to run the rules against, as run(FactStore) takes them
      options - The settings for this run; RunOptions.defaults() changes nothing
      Returns:
      What the run did, as runWithResult(FactStore) reports it
      Throws:
      RuleExecutionException - as run(FactStore) throws it, and with a TimeoutException cause if the run passes its deadline, or an InterruptedException cause if its thread is interrupted, which keeps the interrupt status set
      IllegalArgumentException - as run(FactStore) throws it
      IllegalStateException - if load(List) has not been called, or the engine is closed; or if the run, while borrowing a copy of the rules, found the same rule list closed each time it read the engine's rules again, many times in a row, which means an engine invariant has broken rather than that the call was wrong (a reload in between starts the count again, so reloads alone never cause this)
      NullPointerException - if an argument is null
      StackOverflowError - as run(FactStore) throws it
    • rules

      RuleSetInfo rules()
      Returns the rules the engine has loaded, their checksum and when they were loaded. A run started before a reload finishes with the rules it started with, so compare RunResult.ruleSetChecksum() with RuleSetInfo.checksum() rather than assuming they match.
      Returns:
      The loaded rules, never null. Before the first load(List) it has no rules, the checksum of an empty rule list, and no load time.
      Throws:
      IllegalStateException - if the engine is closed
    • close

      default void close()
      Closes the engine, and releases what its expression languages hold for the rules, such as interpreter contexts. A run holding a copy of the rules finishes normally, and so does one waiting for a copy, because a rule list can't close under a run that has begun borrowing from it: the languages' sessions are closed as each run returns, unless another run is still waiting for a copy of the same rules and takes them, and their compilers once the last run has left and this method has closed the idle sessions. Afterwards, run(FactStore), runWithResult(FactStore), load(List), validate(List) and rules() throw IllegalStateException — as does a run that had read the rules but had not yet begun to borrow a copy when this method closed the engine, unless another run is still using the rules when it borrows: then it uses them, as a run that read rules a reload replaced does, and its rules may run after this method has returned. Otherwise it reads the rules again and finds a closed engine. A run whose thread is interrupted, or whose deadline has passed, stops before reading them again, with a RuleExecutionException, as one stopped while waiting for a copy does. A load() that found the engine open before this method closed it isn't stopped. If it fails, it throws what it would on an open engine, such as RuleCompilationException. If it succeeds, either it swapped its rules in first, and this method closes them like any others, or it finds the engine closed, closes its rules rather than swapping them in, and throws IllegalStateException. A failure that stops this method closing rules part way, as running out of stack in the engine's own steps can, is thrown, fatal or not, and the engine is closed all the same. Closing an engine that is already closed does nothing, unless an earlier call, or a load(), stopped part way that way: then it goes on closing those rules.

      A failure to close a session or a compiler is logged at WARN and not thrown, except a fatal Error, which is rethrown unchanged once every session that was idle when this method closed the rules, and the compilers if no run is using the rules by then (holding a copy, waiting for one, or not yet returned), has been closed: the first, if there are several, carrying the others as suppressed exceptions. The engine is closed all the same, so closing it again does nothing more, unless closing the rules failed part way (see above). A copy given back while this method is still taking the idle copies, before it has marked the rules closed, counts as one of them: this method closes it too if no run is using the rules by then, and the last run to leave does otherwise. A copy a run still holds is closed when the run gives it back, or, when it's kept for a run still waiting for a copy, by that run or the last run to leave, and a fatal error from that reaches the run that closes it, never this method. The compilers are closed once both this method has closed the idle sessions and the last run has left, by whichever finishes second, which gets their fatal error: so when a run leaves while this method is still closing, this method closes the compilers and throws their error. By default, this method does nothing.

      Specified by:
      close in interface AutoCloseable