Drools

Drools is a Java-based business rule management system (BRMS) based on an enhanced implementation of a the Rete algorithm (a pattern matching algorithm developed to efficiently control triggering of rules). KIE (Knowledge Is Everything) is the new umbrella name for Drools and related technologies like jBPM, Guvnor etc and is often interchangeably used with Drools.

Basic structure of a rule written in Drools
A rule engine essentially translates data flowing into a system into decisions taken by the system. In an imperative coding style this often translates into a flowchart-like sequence of if-else conditions. If we visualize the flowchart like a tree, often the same checks may be required at different branches leading to code duplication. Managing this can easily become a nightmare. Even if we avoid code duplication using composition based object oriented patterns, often readability takes a hit. Drools is designed to solve problems like this

Overall, a drools rule engine is a collection of rules where each rule is structured like this :
when
some condition is satisfied
then
perform some action

Unlike imperative programming style where we write sequence of steps that the program follows, Drools follows a declarative programming style where we express the logic for execution without specifying the control flow or the exact ordered sequence of steps.

As a developer we don't directly control the exact execution of rules. We specify a set of rules as in what action to be taken when certain conditions are met and feed them to the rule engine. With every incoming data unit, the engine determines the set of rules to fire based on the conditions met. If there are multiple possibilities, the engine picks one and triggers the corresponding action. Note that as part of a rule's action it is possible to amend the data. As a result, a rule's action can potentially cause new rules to be eligible for being triggered. So every-time a rule is fired, the system needs to evaluate the when conditions for all the rules again. The following diagram summarizes is perfectly :
Image result for drools rule execution life cycle
In order to build an efficient and maintainable rule system, we need to ensure that every rule follows certain basic guidelines or principles :
  1. A rule should depend just on the current state of the data and not depend directly on execution or triggering of another rule
  2. A rule should be simple and atomic. Complex and hierarchical decision trees should be broken down into simpler atomic rules
  3. A rule should never be invoked directly. Only rule engine should invoke a rule
  4. While its possible to impact the order in which the rules are fired, this is best avoided and should be limited to exceptional and very special use cases

Here's an example of a drools rule :
rule "trade must have a trade date"
dialect "mvel"
when
Trade( tradeDate == null )
then
System.out.println("Alert !!! A trade must have a valid trade date")
end

Lets take a deeper look into the syntax:
  • The first line defines the rule name. Every rule in Drools must have a unique name. If rule names aren't unique, when a second rule with the same name as another rule is added to a package it replaces the original rule
  • The next line is an example of rule attribute. The dialect attribute species the language to be used for any code expressions in the LHS or the RHS code block. While the dialect can be specified at the package level, this attribute allows the package definition to be overridden for a rule. Drools currently allows two dialects - MVEL and Java
  • The when condition essentially checks if the data is an instance of Trade and the tradeDate attribute is populated or not. We'll explore this deeper when we look at MVEL syntax
  • The action is pretty straight forward. The action is always written in Java even if the dialect is "mvel"
  • end -> indicates the end of a rule

MVEL vs Java
MVEL is a hybrid dynamic/statically typed, embeddable Expression Language and runtime for the Java Platform. The Java runtime allows MVEL expressions to be executed either interpretively, or through a pre-compilation process with support for runtime byte-code generation to remove overhead.
Since MVEL is meant to augment Java-based software, it borrows most of its syntax directly from the Java programming language with some minor differences and additional capabilities

Here are a few simple examples of MVEL expressions and corresponding Java code :

Reading a bean property
// MVEL
trade.tradeDate

// Java
trade.getTradeDate()

Null safe bean navigation
// MVEL
trade.?product.symbol

// Java
trade.getProduct() != null ? trade.getProduct().getSymbol() : null

Equality check
// MVEL
trade.product.symbol == "AAPL"

// Java
"AAPL".equals(trade.getProduct().getSymbol())

Setting a bean property
// MVEL
trade.product.symbol = "AAPL"

// Java
trade.getProduct().setSymbol("AAPL")

Checking if a list contains an item
// MVEL
trade.product.symbol in ["AAPL", "GOOG", "FB"]

// Java
Arrays.asList({"AAPL", "GOOG", "FB"})

List access
// MVEL
trades[5]

// Java
trades.get(5)

Checking for null
// MVEL
tradeDate == nil
tradeDate == null

// Java
tradeDate == null

Map access
// MVEL
products["AAPL"]

//Java
products.get("AAPL")

BigInteger/BigDecimal Literals : use a B or I suffix
//MVEL
price = 104.39484B // BigDecimal
quantity = 8I // BigInteger

// Java
price = new BigDecimal("104.39484")
quantity = new BigInteger("8")

Value coercion: MVEL's type coercion system is applied in cases where two incomparable types are presented by attempting to coerce the right value to that of the type of the left value, and then vice-versa.
"123" == 123
This expression is true in MVEL because the type coercion system will coerce the untyped number 123 to a String in order to perform the comparison.

Instance of check
// MVEL
t:Trade

// Java
t instanceof Trade

Instance of check combined with condition check:
// MVEL : , is interpreted as && we can explicitly use && as well
t:Trade( !pending, tradeDate != null)

// Java
(t instanceof Trade) && t.isPending() && (t.getTradeDate() != null)

We may write scripts with an arbitrary number of statements using the semi-colon to denote the termination of a statement. This is required in all cases except in cases where there is only one statement, or for the last statement in a script.
statement1; statement2; statement3
Note the lack of semi-colon after statement3.

For full documentation on MVEL refer to http://mvel.documentnode.com

Drools Terminology
Lets have a look at some of the terminologies used in Drools ecosystem. They'll make it easier for us to understand context when reading Drools documentation
  • Fact : refers to the POJO data object that triggers rules
For example, in the rule below :
rule "trade must have a trade date"
dialect "mvel"
when
Trade( tradeDate == null )
then
System.out.println("Alert !!! A trade must have a valid trade date")
end
The when condition essentially checks that the fact is an instanceof Trade and the tradeDate attribute is null
  • Knowledge Base : Is an interface that manages a collection of rules. Typically all rules written within a package (or a list of packages) form a knowledge base
  • Knowledge Session : Is a runtime instance that loads a knowledge base. Facts are inserted at runtime into a knowledge session. A Drools knowledge session can be of two types :
    • Stateless Session: doesn’t track state changes in fact while rules are being fired
    • Stateful Session: any updates to fact while rules are being fired is tracked
  • Salience : Determines the order in which rules are triggered. Higher the salience, greater the priority of that rule being executed
  • Agenda Group : An alternate way to control execution order. Salience is relevant only within an agenda group. If no agenda groups are specified, all the rules are assumed to be part of one single agenda group called ‘main’
  • Activation Group: At most one rule gets triggered within an activation group. If the when clause evaluates to true for one rule (the highest salience) other rules are skipped

Drools configuration
In Drools, a KIE session stores and executes runtime data. The KIE session can be created from a KIE container in the KIE module descriptor file (typically named kmodule.xml)
<kmodule xmlns="http://www.drools.org/xsd/kmodule">
<kbase name="trades" packages="com.test.trades">
<ksession name="testSession" type="stateless" default="true" >
</kbase>
</kmodule>

Initializing session :
KieContainer kieContainer = KieServices.Factory.get().getKieClasspathContainer();
StatelessKieSession kieSession = kieContainer.newStatelessKieSession("testSession");

Drools supports two types of sessions:
  • Stateless Kie Session : Drools doesn't track changes to the state of the fact. So if the change in state enables new rules they wont be triggered
Lets consider an example where we have two rules :
Rule A: if Trade object doesn't have trade date mark the trade as invalid
Rule B: if Trade is invalid, generate an alert email
Clearly we want Rule A to be triggered before Rule B. Even we define higher salience for Rule A, that only impacts the execution order. Because the session is stateless, Drools engine assumes that the rules wont impact the state of the fact (or even if they do it doesn't impact evaluation of rules). so the results from initial evaluation of all the rules (based on which engine decided to invoke Rule A) will be cached and the rules wont be evaluated unless explicitly requested. So rule B wont be triggered because the when condition for Rule B wont be evaluated again after the change in state of the Trade object. So essentially with stateless sessions we cant have action of one rule impacting the when condition of another (unless we force re-evaluation of all the rules)
  • Stateful Kie Session (simply referred to as KieSession) : Tracks changes to the state of the fact. Upon any change in fact, all the rules are rechecked and rerun if necessary

By default Drools assumes all sessions to be stateful. If we want a session to be stateless, we will have to explicitly state type="stateless" in the kmodule.xml file

Typically rules are defined in files with .drl extension. The packages attribute the kbase element in kmodule.xml tells Drools engine where to look for those rule files. All the rule files location in the package(s) configued for a kbase will be part of the rule set for that kbase/kiesession ( So we need to ensure that the rule name uniqueness applies across all files)

To insert a fact into a rule, we can use the execute method. It expects a collection, for for a single fact, we can wrap with with Collections.singletonList(..)
kieSession.execute(Collection.singletonList(trade));

With stateful sessions we need to ensure that we explicitly dispose the facts as well. Else will cause a memory leakage eventually leading to an OutOfMemoryError
KieContainer kieContainer = KieServices.Factory.get().getKieClasspathContainer();
KieSession kieSession = kieContainer.newKieSession("testSession");

for (Trade t : trades){
kieSession.insert(t);
}

kieSession.fireAllRules();
kieSession.dispose();

Binding a variable in Drools :
Lets have a look at the rule example that we looked at :
rule "trade must have a trade date"
dialect "mvel"
when
Trade( tradeDate == null )
then
System.out.println("Alert !!! A trade must have a valid trade date")
end
What if I want a reference to the fact object as part of the action ? In this case it would be extremely helpful to log some of the other attributes of this trade for example.
We can achieve that with bind variables :
rule "trade must have a trade date"
dialect "mvel"
when
$t:Trade( tradeDate == null )
then
System.out.println("Alert !!! A trade must have a valid trade date :"+$t)
end
In this case $t gives us a reference to the fact object. Note that the $ sign has no special meaning and is just a convention

Similarly we can bind an attribute of the fact as well instead of the fact itself:
rule "trade must have a trade date"
dialect "mvel"
when
Trade( tradeDate == null , $id:tradeId)
then
System.out.println("Alert !!! A trade must have a valid trade date :"+$id)
end
Here, $id gives us a reference to the tradeId attribute of the Trade fact

We can use the bind variable to change the state of the fact as well
rule "trade must have a trade date"
dialect "mvel"
when
$t:Trade( tradeDate == null)
then
System.out.println("Alert !!! A trade must have a valid trade date :"+$t)
$t.setInvalid(true);
end

If there are other rules whose when condition get impacted by the action of this rule changing state of the fact and we are using stateless session, we need to force Drools to recompute when clauses for all the rules again:
rule "trade must have a trade date"
dialect "mvel"
when
$t:Trade( tradeDate == null)
then
System.out.println("Alert !!! A trade must have a valid trade date :"+$t)
modify($t){
setInvalid(true);
}
end

Often rules need to be evaluated based on a combination of more than one objects. Drools has a feature called 'cross products' (works very similar to SQL join) that lets us achieve this
rule "invalidate trades on expired products"
dialect "mvel"
when
$t : Trade()
$p : Product( productId = $t.productId , expired=true)
then
modify ($t) {
setInvalid(true);
}
end
On the Java side :
for ( Trade t : trades) {
kieSession.insert(t);
}
for (Product p : products){
kieSession.insert(p);
}
kieSession.fireAllRules();
kieSession.dispose();
While inserting facts we aren't at all linking products and trades here. Drools engine creates a Cartesian product of trade and product combinations and compute the rules against each possible combination.

We can use Drools to dynamically insert a fact into a session as well :
rule "create a Trade when orders match"
dialect "mvel"
when
$o1 : Order (buy=true)
$o2 : Order (buy=false, symbol=$o1.symbol)
then
Trade t = new Trade($o1, $o2);
insert(t);
end
Just like insert(t), we can use insertLogical(t) as well. One advantage of insertLogical is that if later the state of the facts change making the when clause evaluate to false, Drools engine will automatically purge the inserted object (and thus make it eligible for GC)

What if we want to insert a fact only if doesn't exist already ?
We can use the not condition as part of when clause
rule "create a Trade when orders match"
dialect "mvel"
when
$o1 : Order (buy=true)
$o2 : Order (buy=false, symbol=$o1.symbol)
not ( Trade( order1 = $o1, order2 = $o2) )
then
Trade t = new Trade($o1, $o2);
insertLogical(t);
end
Note that this works only if equals and hashCode methods are properly implemented

Controlling order in which rules get fired :

When for a given fact, there are multiple rules that are eligible to get triggered, can we control which one gets triggered first ?

One way to control this is by using the salience attribute. Higher the salience, higher the priority in execution order. If two or more rules have same salience (or if salience attribute is missing) the order of execution is unpredictable.
rule "create a Trade when orders match"
dialect "mvel"
salience 99
when
$o1 : Order (buy=true)
$o2 : Order (buy=false, symbol=$o1.symbol)
then
Trade t = new Trade($o1, $o2);
insert(t);
end

Another way to control execution order is by using agenda groups. We can tie a rule to an agenda group using the agenda-group attribute
rule "create a Trade when orders match"
dialect "mvel"
agenda-group "tier1"
when
$o1 : Order (buy=true)
$o2 : Order (buy=false, symbol=$o1.symbol)
then
Trade t = new Trade($o1, $o2);
insert(t);
end
In the Java code we can explicitly specify which agenda groups will be executed in what order
Agenda agenda = kieSession.getAgenda();
agenda.getAgendaGroup("tier3").setFocus();
agenda.getAgendaGroup("tier2").setFocus();
agenda.getAgendaGroup("tier1").setFocus();
kieSession.fireAllRules();
The agenda groups work like a LIFO buffer or a stack. So, in the above example, rules that are part of "tier1" group will be executed first

If there are agenda groups defined in rules for which we don't invoke setFocus(), Drools engine will treat them like as if they had no agenda group attribute and those rules will be executed after we are done will all agenda groups.

It's legal to use both agenda-group and salience as part of the same rule. If they are used together, salience is applicable only within an agenda group.

Activation Group (also called XOR group):
Sometime we may require only one rule to be fired and skip the rest even if they are eligible. One way to do it is mark a boolean attribute as part of every rule's action and check for that flag as part of every rule's when clause. This is feasible but is quite tedious as well as error prone.

An alternate way to solve this problem is by using a built-in feature called 'activation group'. Drools ensures that within an activation group, only one rule is triggered (the highest salience rule whose when clause evaluates to true). Once a rule is triggered, the remaining rules are simply skipped.

To bind a rule to an activation group, we can use the activation-group attribute:
rule "create a Trade when orders match"
dialect "mvel"
activation-group "group 1"
when
$o1 : Order (buy=true)
$o2 : Order (buy=false, symbol=$o1.symbol)
then
Trade t = new Trade($o1, $o2);
insert(t);
end

Some of the other popularly used rule attributes are :

  • no-loop
When a rule's action modifies a fact it may cause the rule to get activated again, thus causing an infinite loop. Setting no-loop to true will skip the creation of another Activation for the rule with the current set of facts.
  • enabled
If enabled is set to false, rule won't be executed (but will be evaluated!).
Note the value of enabled attribute doesn't necessarily have to be a constant literal (true/false). It can be a dynamic expression as well (eg based on a System property)
  • date-expires
We can also make a rule ineffective after a certain date.
rule "trade must have a trade date"
dialect "mvel"
date-expires "01-Sep-2020"
when
$t:Trade( tradeDate == null )
then
System.out.println("Alert !!! A trade must have a valid trade date :"+$t)
modify($t){
setInvalid(true);
}
end
  • date-effective
Just like date-expires, we can also make a rule applicable only after a certain date. This comes quite handy while dealing with regulatory requirements as deployment date doesn't have to be in sync with regulation effective date.







comments powered by Disqus