Controlling loops in rules

So far, we've seen some ways in which we can manage our rules to trigger new rule invocations. This will help us enormously in order to be able to split our rules into simple components that interact in the background through the data in the working memory. Powerful as it is, however, it can bring us a few extra complications along the line of the rules getting fired more times than we desire. Fortunately, Drools provides us with a set of elements to control rule execution from the very syntax where we define them.

The first and the most simple case where we can get into an infinite rule execution loop happens when a rule modifies the working memory in a way that it retriggers itself. Let's see an example of this problem in the following:

rule "Apply 10% discount on notepads"
    when $i: Item(name == "notepad", $sp: salePrice)
    then modify($i) { setSalePrice($sp * 0.9); }
end

In this rule, our intention is to reduce the sale price of the notepads in our inventory. We just don't change the value of our items, however, we also want to notify the engine that it changed. This is done using the modify keyword, as discussed earlier in this chapter, and we do it as we might have other rules that need to re-evaluate the item now that the price is different.

The problem is that if the modified object still matches the condition of this rule (and it does as its name is still notepad), it will also re-evaluate itself. This will lead to an infinite amount of rule executions for the same element.

The method to avoid this unwanted loop is a very simple attribute called, by no surprise, no-loop. The no-loop rule attribute prevents a rule from reactivating itself, irrespective of the changes the rule makes to the working memory. Its syntax is very simple, as the following example depicts:

rule "Apply 10% discount on notepads BUT ONLY ONCE"
    no-loop true
    when $i: Item(name == "notepad", $sp: salePrice)
    then modify($i) { setSalePrice($sp * 0.9); }
end

The true condition is optional, and writing no-loop is enough. The boolean parameter is there because, as we previously mentioned, it can be a variable from the context that determines whether or not this rule is to be set as no loop or not.

It's worth mentioning that no-loop only prevents this rule from refiring for the same data if it was the last rule to fire. If another rule changes the working memory in a way that matches this rule again, the no-loop condition won't prevent it from executing a second time. Sometimes, this is a desired behavior, however, if this isn't the case, there are other types of loop-prevention strategies that we need to discuss.

Lock-on-active

Let's see an example of how this works:

rule "Give extra 2% discount for orders larger than 15 items"
    no-loop true
    when $o: Order(totalItems > 15)
    then modify ($o) { increaseDiscount(0.02); }
end
rule "Give extra 2% discount for orders larger than $100"
    no-loop true
    when $o: Order(total > 100.00)
    then modify ($o) { increaseDiscount(0.02); }
end

These rules have the no-loop attribute so that even if they modify the order object, they won't retrigger themselves. However, nothing is stopping them from activating each other. Therefore, the first rule will trigger the second one, which will trigger the first one again, and so on and so forth. This type of infinite loop requires something a bit stronger than the no-loop attribute.

One quick way of making sure that a rule doesn't get retriggered for the same objects is to add an attribute called lock-on-active to the troublesome rules. Whenever a ruleflow-group becomes active or an agenda-group receives the focus, any rule within this group that has lock-on-active set to true will not be activated any more for the same objects. Irrespective of the origin of the update, the activation of a matching rule is discarded. The following is an example of one of the rules rewritten to use lock-on-active:

rule "Give extra 2% discount for orders larger than $100"
    lock-on-active true
    when $o: Order(total > 100.00)
    then modify ($o) { increaseDiscount(0.02); }
end

In this second case, the rules will trigger only once for the same objects. You can see a running example of these rules in the LoopingExamplesTest class of the chapter's code bundle.

This is a stronger version of no-loop as the change can now be caused not only by the rule itself. It's ideal for calculation rules, where you have a number of rules that modify a fact and you don't want any rule re-matching and firing again. Only when the ruleflow-group is no longer active or the agenda group loses the focus; these rules, with lock-on-active set to true, become eligible again for matching on the same objects to be possible again.

Model properties execution control

The no-loop and lock-on-active attributes give us a great amount of power when it comes to controlling undesired execution loops. The problem with these attributes, however, arises from the fact that the whole objects used in the condition will not retrigger the rule, no matter how much they are modified by the same or other rules. This might not be the desired behavior for a lot of cases where the model is complex or hard to change and we might still need to re-evaluate some of the changes if they occur to specific properties.

This sort of fine-grained control is possible, in its most simple form, by adding flag attributes to the objects that store the conditions that we want to check. Some rules will alter these flags and some other rules will check against them to see whether they should or should not evaluate again. Here's an example:

rule "Add 2% discount for orders larger than $100"
  when $o: Order(total > 100.00, has100DollarsDiscount == false)
  then
    modify($o){
      increaseDiscount(0.02);
      setHas100DollarsDiscount(true);
    }
end

rule "Add 2% discount for orders larger than 15 items"
  when $o: Order(total > 100.00, has15ItemsDiscount == false)
  then
    modify($o){
      increaseDiscount(0.02);
      setHas15ItemsDiscount(true);
    }
end

In the previous rule, there was no need to add the rule attributes to the rules as the use of two flag attributes (has15ItemsDiscount and has100DollarsDiscount) had already flagged the object to not be evaluated again. If any other modification was done to the object in the working memory, it would retrigger the rule.

This solution has two main problems. One problem is that we will eventually saturate the model with extra properties with no direct relation to the actual content of the model, however, they are more related to the execution of rules. The second problem arises when we have so many rules that we need to check on too many flags in order to make the rule easy to understand. Remember our main goals when writing rules: keep them independent and as simple (atomic) as possible. If we have too many flags that relate to specific rules, the independence starts to break.

Let's not worry. There are more tricks within Drools that will help us with these situations.

Declared types

Drools rules conditions are build, based on Java types. This means that we need to have a defined set of Java classes to define our model and use it from our rules. All our previous examples have been based on the existence of Java classes to represent orders, discounts, clients, and so on.

This is not the only way that Drools has to define a data model. Within the DRL files, where we define our rules, we can also define new types that will be created, compiled, and made available in the runtime at the same moment as the rules and it can be changed just as easily. These are called declared types and they play a very useful part in defining data models that only make sense for specific groups of rules (such as inference objects that might not necessarily be a part of the rules output result). They are defined before the rules in the DRL structure, with the following syntax:

declare SpecialOrder extends Order
      whatsSoSpecialAboutIt: String
      order: Order
      applicableDiscount: Discount
   end

The previous example contains a few things all together that are worth mentioning, as follows:

  • They are defined between the declare and end keywords
  • They can define object attributes (such as String) and primitive attributes (such as long)
  • They can extend other types, including Java classes and other declared types
  • They can even declare attributes that are a part of your own model (such as the applicableDiscount attribute in the previous example, of type Discount) and they can also have other declared types as attributes

These types can be used from the conditions and consequences of the rules in the same way as any other Java class. You can access the getters/setters of all the attributes that will be automatically generated by the rule engine at the compilation time. The only difference arises from trying to access these objects from outside the rules in the plain Java code. It is possible to do so, however, it requires using a reflection API accessible through the KieBase, as follows:

KieSession ksession = ...; //previously initialized
FactType type = ksession.getKieBase().getFactType("chapter04.declaredTypes", "SpecialOrder");
Object instance = type.newInstance();
type.set(instance, "relevance", 2L);
Object attr = type.get(instance, "relevance");

Due to this complex syntax for use in Java code, it is best to use declared types only if they're going to be used exclusively from the rules code. The use of declared types can be seen in the DeclaredTypesTest class in the chapter-04/chapter-04-tests project of the code bundle.

Property-reactive beans

Whenever we use the modify or update keywords in the consequence of a rule, we're notify the engine that rules that filter similar object types should re-evaluate the object again. This re-evaluation, by default, occurs on the whole object; As long as one property of the object changes, the rules will consider it as a new object to match.

This could lead to some issues when we don't wish to have a rule re-evaluated for some changes. The loop-control mechanisms, such as no-loop and lock-on-active, could be helpful in these situations. However, if we want the rule to control changes on some properties only, we need to write very complex conditions. Also, if the model changes in the future for a large rule base, you may have to modify a lot of rules to avoid undesired rule re-executions.

Fortunately, Drools provides a feature that allows the engine to work around this problem. It allows the rule writers to define the attributes of a bean that should be monitored if they're updated in the working memory. This feature is defined in the data model (Java classes or declared types) that are used in the rules and it is called property-reactive beans.

To use this feature, we first need to mark the types that we will use in the rules with the Property Reactive annotation. This annotation allows the engine know that whenever an object of this type is added to the working memory, special filtering on its changes needs to apply. This annotation can be added to a Java class (at class level) or declared type (right after the first line, defining the type name), as follows:

   declare PropertyReactiveOrder
       @propertyReactive
       discount: Discount
       totalItems: Integer
       total: Double
   end

After we have our types "marked" as property-reactive beans, we can make our rules define the properties of these beans that should be monitored for changes, and the properties that should not. To do so, we use the @Watch annotation after each specific condition of the rule that should have this type of filtering applied, as shown in the following:

rule "Larger than 20 items orders are special orders"
       when$o: PropertyReactiveOrder(totalItems > 20) @Watch (!discount)
       thenmodify ($o) { 
               setDiscount(new Discount(0.05));
           }
   end

In the previous rule, the @Watch annotation makes the rule behave similar to adding a no-loop or a lock-on-active rule attribute. It will avoid this and the other rules to be re-fired for the same element. However, if any rule modifies the object in a way other than changing its discount, it will not avoid the retriggering of this rule. This is the main power of the property-reactive beans; you can determine which attribute changes could retrigger the rule and which could not.

The @Watch annotation can be used in the rule to monitor different situations regarding the attributes of a property-reactive bean. If not present, the watched fields can be inferred by default, depending on the condition structure. Here, we can see a few examples:

  • @Watch(discount, total): This only monitors changes in the discount and total attributes
  • @Watch(!discount): This means that we should disregard changes to the discount attribute of the bean
  • @Watch(!*, total): This means that we should disregard all the attributes of the bean, except for the total attribute
  • @Watch(!*, total, totalItems): This means that we should only pay attention to changes of the total and totalItems attributes, other changes are disregarded

Property reactive beans changes should be notified to the rule engine only using the modify keyword. The update keyword won't be able to tell the difference between the attributes of the bean that are being changed. This is another reason to discourage the use of the update keyword.

We've seen, so far, how malleable and extensible the data model that we use in our rules is. Based on its modifications and the conditions that our rules define, we can create very complex environments in very simple ways. The next thing we need to learn is how to define the conditions in our rules in a simple yet powerful way. To do so, let's take a look at some of the most used operations that we can use to compare our data in our rule definitions.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.117.142.144