Interface RulesEngine<O>
- Type Parameters:
O- The output object type to instantiate
- All Superinterfaces:
AutoCloseable
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 TypeMethodDescriptiondefault voidclose()Closes the engine, and releases what its expression languages hold for the rules, such as interpreter contexts.voidCompiles a rule list and swaps it in for the rules loaded before, if any.rules()Returns the rules the engine has loaded, their checksum and when they were loaded.default @Nullable ORuns the loaded rules against facts.runWithResult(FactStore<?> facts) Runs the rules likerun(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'stagsandstart instant.runWithResult(FactStore<?> facts, RunOptions options) Runs the rules likerunWithResult(FactStore), with settings for this run only, such as atimeoutthat replaces the one the engine was built with.Compiles a rule list exactly asload(List)would, with this engine's languages, imports, options and declared facts, without loading it, and returns the problemsload()would have thrown instead of throwing.
-
Method Details
-
load
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
InterruptedExceptionin 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 fatalError, which is thrown as described below (seeSession.close()).- Parameters:
ruleList- The list ofRuleobjects- Throws:
RuleCompilationException- if the list contains anullrule; 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 returnsnullinstead of a compiler or a compiled expression; aLinkageErrorsuch as aNoClassDefFoundErroris 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 holdingnull(seeExpressionCompiler.factNamesRead()); or adeclared facthas 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 (seeExpressionLanguage.reservesForEveryRuleList()). Anullrule or a duplicate name is thrown at once. The other problems are collected before the exception is thrown, andfailures()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 withcopiesAtLoad(n)makes its copies of them, and a language that throws while creating or warming up a session for one, or returnsnullinstead 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 loadNullPointerException- ifruleListitself isnullStackOverflowError- 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 withServiceLoaderwithout its builder naming it, and the thread has too little stack left to prepare it (seeExpressionLanguage.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 rulesError- aVirtualMachineErrorother thanStackOverflowError, and other than anInternalErrorthat wraps one with noLinkageErrorbetween 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 anOutOfMemoryErrorthe 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, asclose()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 nextload()orclose()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. AThrowablethat is neither anExceptionnor anErroris never fatal: it's handled like an exception from the same place, reported as aRuleCompilationExceptionwhile compiling or making copies, and logged at WARN while closing.
-
validate
Compiles a rule list exactly asload(List)would, with this engine's languages, imports, options and declared facts, without loading it, and returns the problemsload()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 laterload()compiles the list again. Nothing about the rules is logged, not even a language's compile warnings; a fatalErroris 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 returnsnullinstead of a session, fails thatload(), naming the language. With the defaultcopiesAtLoad(0)nothing is outside what it can see.The problems come in the order
load()finds them: anullentry 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. Unlikeload(), anullentry or a duplicate name doesn't stop the check: every other rule is still compiled.- Parameters:
ruleList- The list ofRuleobjects- Returns:
- One exception for each problem found, as
load()would have reported it; empty when it finds none - Throws:
IllegalStateException- if the engine is closedNullPointerException- if the list itself isnullStackOverflowError- asload(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 useError- aVirtualMachineErrorother thanStackOverflowError, and other than anInternalErrorthat wraps one with noLinkageErrorbetween 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
Runs the loaded rules against facts.- Parameters:
facts- The facts to run the rules against. AnyFactStoreis accepted, such as aFactMap<Applicant>. The engine reads them throughFactStore.asMap().- Returns:
- The output of the actions of the matching
Ruleobjects, ornullif the rule list is empty or no rule matched - Throws:
RuleExecutionException- if a condition or action throws; a condition evaluates tonullor a non-boolean; an action returnsnullinstead of anActionResult, or a property it returned can't be set on the output; the output supplier throws or returnsnull; a language throws or returnsnullwhen it creates a session for the run; or more than one rule matches on aunique-matchengine, 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 orclose()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 anInterruptedException, or because it passed the deadline atimeoutgave it, which makes the cause aTimeoutException. Either belongs to no rule, sogetRuleName()isnull.IllegalArgumentException- if a fact's name isnull, blank, or one of the engine's languages reserves (seeExpressionLanguage.reservedFactNames(),outputby default; a language that reserves its names only for the rule lists that use it, asExpressionLanguage.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, asExpressionCompiler.factNamesRead()says); if adeclared facthas 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, withRulesEngineBuilder.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- ifload(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- iffactsisnullStackOverflowError- if the thread has too little stack left for the run's own steps, checked before the run takes anything; more is checked for, afterfactsand 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 wasError- aVirtualMachineErrorother thanStackOverflowError, and other than anInternalErrorthat wraps one with noLinkageErrorbetween 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 atry-with-resources whoseclose()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 orclose()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 orclose()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 anOutOfMemoryErrorthe 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 fromonRunErroris thrown in place of the run's failure, which it carries. Every otherErrorfrom a rule, the output supplier, an output writer or a language creating a session, including aLinkageError, is reported as aRuleExecutionException; one from a language's check of a fact name as anIllegalArgumentException; one from a listener is logged, and the run goes on; one from closing a session is logged at WARN. AThrowablethat is neither anExceptionnor anErroris never fatal: wherever it arises, it's handled like an exception from the same place.
-
runWithResult
Runs the rules likerun(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'stagsandstart instant. A caller can record which rules produced a decision, and why the others didn't apply, without aRuleListener.- Parameters:
facts- The facts to run the rules against, asrun(FactStore)takes them- Returns:
- What the run did, never
null. Itsoutputisnullexactly when no rule fired, because the rule list is empty or no condition matched, and itsevaluationshave one entry per loaded rule. - Throws:
RuleExecutionException- asrun(FactStore)throws itIllegalArgumentException- asrun(FactStore)throws itIllegalStateException- ifload(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- iffactsisnullStackOverflowError- asrun(FactStore)throws it
-
runWithResult
Runs the rules likerunWithResult(FactStore), with settings for this run only, such as atimeoutthat 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; seeRulesEngineBuilder.runTimeout(Duration)for what that does and doesn't stop.- Parameters:
facts- The facts to run the rules against, asrun(FactStore)takes themoptions- The settings for this run;RunOptions.defaults()changes nothing- Returns:
- What the run did, as
runWithResult(FactStore)reports it - Throws:
RuleExecutionException- asrun(FactStore)throws it, and with aTimeoutExceptioncause if the run passes its deadline, or anInterruptedExceptioncause if its thread is interrupted, which keeps the interrupt status setIllegalArgumentException- asrun(FactStore)throws itIllegalStateException- ifload(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 isnullStackOverflowError- asrun(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 compareRunResult.ruleSetChecksum()withRuleSetInfo.checksum()rather than assuming they match.- Returns:
- The loaded rules, never
null. Before the firstload(List)it has no rules, the checksum of an empty rule list, and noload 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)andrules()throwIllegalStateException— 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 aRuleExecutionException, as one stopped while waiting for a copy does. Aload()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 asRuleCompilationException. 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 throwsIllegalStateException. 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 aload(), 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:
closein interfaceAutoCloseable
-