Esteban's Blog

/dev/rnd

About Drools and Infinite Execution Loops

Infinite recursion is a typical problem when we are working with Drools (and possibly any other rule engine). The inference mechanism going on inside Drools requires rules to be re-evaluated and executed as needed. This powerful feature about rule engines could be really beneficial some times, but some other times it could be a really PITA. I’ll try to analyse in this post the origins of this recursion and the most common ways we have to deal with them in Drools: control fields, control facts, rule attributes and Fine grained property change listeners.

When we are authoring rules we must take extra care for 2 different situations that could lead to infinite recursion:

  1. Fact modifications from within a rule could create a new activation of the same rule (self-loop).
  2. Fact modifications from within a rule could activate a different rule that will eventually re-activate the original rule (complex-loop).

Let’s analyse how we can deal with these 2 types of recursion in Drools.

Self-loops

Dealing with the first type of recursion (self-loop) most of the times is trivial. We just need to make sure that the constraints present in the rule are exhaustive enough to avoid re-activations caused by the right hand side (RHS) of the rule. So, for example if we have a rule saying that Customers with more than 3 years seniority have a 10% discount we could start with a rule of the type:

rule "Apply more than 3 years seniority discount"  //Case1.java
when
    $c: Customer(seniority > 3)
then
    modify($c){
        setDiscount($c.getDiscount()+0.1),
        addDiscountReason("Seniority grater than 3 years")
    }
end

Of course this is a very basic rule, but it has some serious problem though. The problem is that the modification of the Customer fact in the RHS will generate a new activation and execution of the same rule. This is a very common problem most of us faced when started with Drools.

The first solution (and maybe the most intuitive one) is to avoid the use of modify() in the RHS of the rule. Without the modify() call, Drools will never know about the change in Customer fact and the rule is not going to be re-evaluated and re-activated again. Unfortunately, this magical solution has its own disadvantages and sometimes we can’t just go for it. Let’s suppose for example that we have another rules saying that no Customer (no matter his seniority)  could have a discount grater than 25%. This rule may look like this:

rule "No Customer has a discount grater than 25"
when
    $c: Customer(discount > 0.25)
then
    modify($c){
        setDiscount(0.25)
    }
end

If we were not using a modify() in the rules that apply discounts, latter rule will never be activated. In these type of situations, letting Drools know about the modification in certain facts is desired and even required.

Now that we have described the problem, let’s analyse 3 different ways to deal with self-loop problems in Drools: control fields, control facts and the usage no-loop attribute

Control fields: Narrow down Rule’s Constraints to avoid re-activation

Sometimes, the easiest solution is to narrow  down the constraints of the rule to exclude the already modified facts. In the case of “Apply more than 3 years seniority discount” rule we could use the list of discount reasons of Customer fact to check whether the current discount was already applied or not:

rule "Apply more than 3 years seniority discount"  //Case2.java
when
    $c: Customer(seniority > 3, discountReasons not contains "Seniority grater than 3 years")
then
    modify($c){
        setDiscount($c.getDiscount()+0.1),
        addDiscountReason("Seniority grater than 3 years")
    }
end

The downside of this elegant solution is that we have to analyse what we are modifying in the RHS of each rule to check if the conditions of the rule are prepared to deal with them or not. A change in the RHS could lead to changes in the Conditions of the rule. Maintaining these kind of rules is most of the times a complex task. If your rules are not going to change over time or if the change rate is not elevated you can evaluate whether this is a good solution for you or not.

A common technique commonly used when using control fields is to create rule-specific fields in the fact classes instead of using business-related fields.

Control Facts: Narrow down Rule’s Constraints without polluting your Facts

Using control fields is a good approach but as we saw it has some flaws. Sometimes we don’t have any good candidate among the fields of our fact classes to be used as a control field. In that case, as we saw earlier, the creation of a specific field in our fact classes could be a solution. But even with a specific control field things could get complex. The first obvious problem is that we are polluting our facts with fields that are not business-specific. Even if we are ok with that, using control fields gets more complicated with rules involving more than 1 fact. In those cases, we usually require 1 control field for each involved fact and, even worst, the value of these fields must be related to the combination of facts.

A good way to overcome some of the limitations control fields have is to use control facts instead. A control fact instance will represent the execution of a specific rule for a specific fact set. Instead of modifying the structure of our fact classes or add specific constraints to each of the patterns in our rules we could use a single fact instance for this:

rule "Apply more than 3 years seniority discount"  //Case3.java
when
    $c: Customer(seniority > 3)
    not ThreeYearsSeniorityDiscountApplied(customer == $c)
then
    modify($c){
        setDiscount($c.getDiscount()+0.1),
        addDiscountReason("Seniority grater than 3 years")
    }

    insert (new ThreeYearsSeniorityDiscountApplied($c));
end

In the latter rule we can verify how the Customer pattern is clean and simple again. We have moved the re-activation prevention part into a specific fact class called ThreeYearsSeniorityDiscountApplied. The RHS of the rule is in charge of the insertion of one of this facts after a customer was processed. In this case, changes in the RHS don’t require a change in the LHS of the rule since the lock is not implemented in the Customer pattern but in our special fact.

Since there is not such a thing as a perfect solution, this approach has its own drawbacks too.  If we follow the recipe to the letter, we will need to create a specific control fact for each of the rules we have. An improvement over this solution could be to use a generic control fact with some field specifying the rule for which it applies:

rule "Apply more than 3 years seniority discount"
when
    $c: Customer(seniority > 3)
    not DiscountApplied(type == "3 years", customer == $c)
then
    modify($c){
        setDiscount($c.getDiscount()+0.1),
        addDiscountReason("Seniority grater than 3 years")
    }

    insert (new DiscountApplied("3 years", $c));
end

But, even if this solution is more flexible and maintainable than having 1 control fact for each rule, it has the problem that the RHS and LHS depends (in a low degree) on each other; a typo in one of the “3 years” strings will create an infinite loop during runtime.

no-loop attribute

Since self-loops are something very common to find in our rules, Drools already comes with a special attribute we can use in our rules to avoid these situations: no-loop.

A rule marked with the no-loop attribute will not be re-activated as a direct consequence of the execution of its RHS. So, no matter what we do in the RHS of the rule, it is never going to be re-activated again:

rule "Apply more than 3 years seniority discount" //Case4.java
no-loop true
when
    $c: Customer(seniority > 3)
then
    modify($c){
        setDiscount($c.getDiscount()+0.1),
        addDiscountReason("Seniority grater than 3 years")
    }
end

Too good to be true, right? No unnecessary fields in our facts, no unnecessary fact classes nor conditions in our rules. The RHS is concise and only deals with business logic.  For self-loop problems, the use of no-loop is indeed an elegant solution. So, if we have isolated rules creating infinite self-loops we should consider to use this attribute. Of course we should never abuse of this (or any) attribute; each time we use an attribute in a rule, we are moving one step away from the declarative approach Drools represents.

As an introduction for complex-loops, let’s analyse in the following section the most important limitation of no-loop: it serves for individual rules only.

Complex-loops

Let’s say we have now another rule in our knowledge base stating “Customers with more than 5 years seniority have an extra 10% discount”. So, a customer with 6 years seniority will have a 20% total discount:

rule "Apply more than 3 years seniority discount" //Case5.java
no-loop true
when
    $c: Customer(seniority > 3)
then
    modify($c){
        setDiscount($c.getDiscount()+0.1),
        addDiscountReason("Seniority grater than 3 years")
    }
end

rule "Apply more than 5 years seniority discount"
no-loop true
when
    $c: Customer(seniority > 5)
then
    modify($c){
        setDiscount($c.getDiscount()+0.1),
        addDiscountReason("Seniority grater than 5 years")
    }
end

In the latter case, the use of no-loop is not enough. Let’s analyse what happens when “Apply more than 3 years seniority discount” rule is executed. The execution of this rule modifies the customer object and lets Drools to know about it. This action would generate 2 new activations: a new activation for “Apply more than 3 years seniority discount” (discarded because of the use of no-loop) and an activation of “Apply more than 5 years seniority discount” rule. So far so good; we have avoided the first infinite loop. The problem is when the activation of “Apply more than 5 years seniority discount” is executed. Since this rule is also modifying the customer fact, 2 new activations should happen: a new activation of itself (avoided thanks to the no-loop attribute) and an activation of “Apply more than 3 years seniority discount” again. And here is where the infinite loop appears. The loop is not related to a single rule anymore; in this case is the presence of the 2 discount rules what generates the loop. In more complex situation is common to observe these kind of complex-loops involving several layers of rules.

So let’s see what are the options we have to mitigate this issue.

Control Fields and Control Facts

Just like with self-loops, the use of control fields and control facts is a valid solution for this situation. A version of the last 2 rules using control facts could look like this:

rule "Apply more than 3 years seniority discount" //Case6.java
when
    $c: Customer(seniority > 3)
    not DiscountApplied(type == "3 years", customer == $c)
then
    modify($c){
        setDiscount($c.getDiscount()+0.1),
        addDiscountReason("Seniority grater than 3 years")
    }

    insert( new DiscountApplied("3 years", $c) ); 
end

rule "Apply more than 5 years seniority discount"
when
    $c: Customer(seniority > 5)
    not DiscountApplied(type == "5 years", customer == $c)
then
    modify($c){
        setDiscount($c.getDiscount()+0.1),
        addDiscountReason("Seniority grater than 5 years")
    }

    insert( new DiscountApplied("5 years", $c) );
end

The same benefits and disadvantages we found for control fields and facts in self-loops apply here.

lock-on-active attribute

For some situations where complex-loops appear, Drools comes with a handy attribute we may use: lock-on-active. This attribute, that is described as a “Stronger version of no-loop” in Drools’ documentation, could be used to avoid infinite execution loops involving one or more rules. But we need to understand its behavior so we can know whether we can use it or not in a specific situation.

What lock-on-active basically does is to prevent a rule to be activated if the agenda group where the rule is defined is already active.  So, let’s see how a version of our discount rules looks like when using lock-on-active attribute. In this case we are also going to be adding a new discount rule for the following case: “Customers with more than 3 children receive an extra 10% discount”:

rule "Apply more than 3 years seniority discount" //Case7.java
lock-on-active true
when
    $c: Customer(seniority > 3)
then
    modify($c){
        setDiscount($c.getDiscount()+0.1),
        addDiscountReason("Seniority grater than 3 years")
    }
end

rule "Apply more than 5 years seniority discount"
lock-on-active true
when
    $c: Customer(seniority > 5)
then
    modify($c){
        setDiscount($c.getDiscount()+0.1),
        addDiscountReason("Seniority grater than 5 years")
    }
end

rule "Apply more than 3 children discount"
lock-on-active true
when
    $c: Customer(childrenCount > 3)
then
    modify($c){
        setDiscount($c.getDiscount()+0.1),
        addDiscountReason("More than 3 children")
    }
end

In this case, everything works as expected; after the execution of these 3 rules, a customer with more than 5 years seniority and more than 3 children will receive a discount of 30%. Every time the customer is modified by one of the rules, no new activation is created because all of the rules are defined in the same agenda group (MAIN) and at the time the modification occurs the group is already active. Just like with no-loop, things seem to resolve in a magical way once again: no bloating fields nor facts, rules that only “talk” about business. Fantastic! Isn’t it?

But what about the rule that prevented a customer to have a discount grater than 25%? Do we need to use lock-on-active in that rule too? After all, the RHS of the rule is modifying the customer and could lead to infinite complex-loops too. Let’s see what happens if we add the lock-on-active attribute to it:

rule "No Customer has a discount grater than 25" //Case8.java
lock-on-active true
when
    $c: Customer(discount > 0.25)
then
    modify($c){
        setDiscount(0.25)
    }
end

Contrary to what we would’ve expected, using lock-on-active in this situation doesn’t generate the expected result. If we insert a customer with more than 5 years of seniority and more than 3 children and we fire all the rules, the final discount is still 30%. How is that possible? The important thing to understand here is when the rules are evaluated and when an agenda group becomes active. After we insert the mentioned customer in the session, all the rules get evaluated; the result are 3 activations (The latter rule is not being activated since the current discount of the customer is 0%). Once we call fireAllRules(), the agenda group having the focus (by default ‘MAIN’ agenda group) will be activated and one of its activations is going to be selected and executed. After this point no new activations could happen for rules belonging to the active agenda group and having lock-on-active attribute defined.  That is why after the third discount rule is executed, the 25% limit rule is not going to be activated even if the current state of the objects in the session match its constraints.

For this particular situation, a simple solution could be to remove the lock-on-active attribute from the 25% limit rule. This will allow its activation after the third discount rule is executed. For more complex situations, things may not be so easy. The important point here is to understand the behavior of lock-on-active to see whether it is a good candidate to be used by us in a specific situation or not.

Property Reactive Facts

Let’s go back for a minute to our original rule and let’s analyse from a different perspective what is “wrong” with it (from a rule engine point of view of course):

rule "Apply more than 3 years seniority discount" //Case9.java 
when
    $c: Customer(seniority > 3)
then
    modify($c){
        setDiscount($c.getDiscount()+0.1),
        addDiscountReason("Seniority grater than 3 years")
    }
end

The “problem” with this rule is that when we modify a fact object, Drools doesn’t know what we have modified in it, and thus re-evaluates all the rules associated with it to find new matches. The infinite loop happens because among the re-evaluated rules we find the same rule that have modified the fact. This behavior is as explained because, since its beginnings, Drools has treated the RHS of a rule as a black box. Fortunately for us, things have changed since version 5.4 and now we have a feature called “Property Reactive” or “Fine grained property change listener” that allows Drools to understand a little bit more about what is going on inside the RHS of our rules, or at least inside the modify() operation.

The idea behind “Property Reactive” facts is that rules should only react upon “interesting” changes in the facts they are matching and not upon every single modification. One of the ways we have to enable this feature is to mark our fact classes with Drools’ @PropertyReactive annotation. When a fact marked with this annotation is found in a rule, the rules automatically adds a “property change listener” to the attributes of this class being used in the rule. The rule will then react only on changes affecting those attributes:

@PropertyReactive
public class Customer {
    ...
}

In the case of our initial rule, a property change listener is going to be automatically configured for Customer.seniority field. Any modification on the fact that doesn’t affect its seniority field is not going to cause a re-evaluation of this pattern avoiding this way a possible re-activation of the rule. Property change listeners can be explicitly defined and overwritten in the same rule using the @Watch annotation:

when
    $c: Customer(seniority > 3) @Watch(childrenCount) 
then

In this case, the pattern will react upon modifications in seniority (automatic) and childrenCount changes. Using wildcards we can listen to any (@Watch(*)) or no change at all (@Watch(!*)).

By using @PropertyReactive annotation in our Customer fact class we don’t need to do anything special in our discount rules: no need for custom fields nor facts and no need for special rule attributes neither. In the case of the 25% limit rule, the use of property specific doesn’t bring any advantage. The particularity of that rule is that it modifies the same field that it is listening to: discount. The self-loop here is prevented by the fact that the condition of the rule and the changes performed to the customer fact in its RHS are mutually exclusive. Setting the discount of the customer to 25 will immediately avoid a re-activation of the same rule: it is only interested in customers with a discount GRATER than 25.

Things would have been different if the condition of the rule would have been “seniority >= 0.25″. In this case, even with property reactive facts, we are going to experience an infinite execution self-loop here. One possible solution for this hypothetical situation could be the use of the no-loop attribute.

Conclusion

We have covered the reasons and types of infinite execution loops we could find in Drools. According to the nature of the loop; self-loops or complex-loops, we have analysed the different ways we have to avoid them. As always, the conclusion is that there is no silver bullet here. The best way to deal with this issue is to know what Drools provides to mitigate it and to analyse the best solution for our particular situation. Most of the times an homogeneous solution is not possible and we have to appeal to a mixed solution.

The code used in this post could be found in my github account: https://github.com/esteban-aliverti/DroolsRecursionPost

About these ads

2 comments on “About Drools and Infinite Execution Loops

  1. Julius
    July 15, 2013

    Brilliant article.

  2. Pingback: ¿Cómo evitar bucles infinitos en Drools? | Antonio Mendoza Pérez

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Information

This entry was posted on November 16, 2012 by in drools, java, plugtree and tagged , , , , , , .
Follow

Get every new post delivered to your Inbox.

Join 163 other followers

%d bloggers like this: