Kie runtime components

Drools presents us with several configuration options for its sessions—whether they are stateless or stateful. In this section, we are going to cover some of the options that we have in order to configure our sessions in a way that allows us to make full use of Drools' potential.

The most common way we usually interact with a Drools session is by inserting/modifying/retracting facts from it and executing any rule activation that may have happened as a consequence of these operations. All these operations target different aspects of the rule engine—such as knowledge assertion and inference—but there are also some other ways to interact with a session that can be used to provide or extract information to or from it. These operations are more oriented to the application where Drools is running and not to the rule engine itself. The options that we are going to review in this section are globals, channels, queries, and event listeners.

Note

Even if the four options are available in both stateless and stateful sessions, we are going to focus on the examples of stateful ones.

Globals

Global variables were briefly mentioned in Chapter 3, Drool Runtime and explained in greater detail in Chapter 4, Improving Our Rule Syntax. In this section, we are going to cover the most common patterns of globals usage inside a session.

Even when globals can be used internally in a session and never be exposed to the outside world, they are typically used as a way to introduce/extract information to/from a session. A global is, in many cases, a contact point between a session and the external word.

When working with stateful sessions, there are three methods in the KieSession class that are related to globals. These methods are shown in the following table:

Method

Description

void setGlobal(String identifier, Object value)

This method is used to set the value of a global. Invoking this method more than once in the same session will update any previously set value of the global.

The identifier used in the invocation of this method must match the identifier (name) of the global in the knowledge base.

Globals getGlobals()

This method is used to retrieve all the globals in a session. The resulting object can be used to retrieve individual globals by their identifiers.

Object getGlobal(String identifier)

This method is used to retrieve a global using its identifier.

As you can see, there is not too much to learn about how to interact with global variables inside a session. The three methods described in the preceding table are almost self-explanatory.

There are four common ways to use a global in Drools, as shown in the following:

  • In the LHS of a rule, as a way to parameterize the condition of a pattern
  • In the LHS of a rule, as a way to introduce new information in a session
  • In the RHS of a rule, as a way to collect information from a session
  • In the RHS of a rule, as a way to interact with external systems

No matter how a global is used in a session, it is important to notice that a global is not a fact. Drools will treat globals and facts in a completely different way; changes in a global are never detected by Drools, and thus, Drools will never react upon them.

Tip

Globals in Drools are not facts! Drools will never be notified nor react when a global is set or modified.

Let's analyze each of the four common scenarios for a global that we have previously listed.

Globals as a way to parameterize the condition of a pattern

One way globals are normally used in Drools is as a way to externally parameterize the condition of a rule. The idea is to use globals instead of hardcoded values in the conditions of our rules.

As an example, let's go back to our eShop example. Let's say that we want a Drools session to detect suspicious operations for customers in our eShop application. We will define a suspicious operation as a customer with pending operations summing more than 10,000 dollars.

The input of our session will be the customers of our application and their orders. For each customer with pending orders for more than 10,000 dollars, we are going to insert a new object of the SuspiciousOperation type. The SuspiciousOperation class has the following structure:

public class SuspiciousOperation {    
    public static enum Type {
        SUSPICIOUS_AMOUNT,
        SUSPICIOUS_FREQUENCY;
    }    
    private Customer customer;
    private Type type;
    private Date date;
    private String comment;

    public SuspiciousOperation(Customer customer, Type type) {
        this.customer = customer;
        this.type = type;
    }
  
    //setters and getters
}

The following rule is enough to accomplish our goal of detecting suspicious operations:

rule "Detect suspicious amount operations"
when
    $c: Customer()
    Number( doubleValue > 10000.0 ) from accumulate (
        Order ( customer == $c, state != OrderState.COMPLETED, $total: total),
        sum($total)
    )
then
    insert(new SuspiciousOperation($c, SuspiciousOperation.Type.SUSPICIOUS_AMOUNT));
end

The rule is straightforward: for each Customer, it collects any Order whose OrderState is not COMPLETED and calculates the sum of their totals. If the total is more than 10,000, then the rule is activated. When the RHS of the rule is executed, it will insert a new object of the SuspiciousOperation type in the session.

As we already know, if we want to execute this rule, we need to include it as part of a knowledge base, create a session from it, and provide some facts to it, as follows:

    //Create a customer with PENDING orders for a value > 10000
    Customer customer1 = new CustomerBuilder()
                .withId(1L).build();
    Order customer1Order = ModelFactory.getPendingOrderWithTotalValueGreaterThan10000(customer1);

    //Create a customer with PENDING orders for a value < 10000
    Customer customer2 = new CustomerBuilder()
                .withId(2L).build();	
    Order customer2Order = ModelFactory.getPendingOrderWithTotalValueLessThan10000(customer1);

    //insert the customers in a session and fire all the rules
    ksession.insert(customer1);
    ksession.insert(customer1Order);
    ksession.insert(customer2);
    ksession.insert(customer2Order);

    ksession.fireAllRules();

A running example of the preceding code can be found in the code bundle under the chapter-05 module.

The previous example works fine as long as the threshold for what we consider a suspicious operation remains unchanged. However, what if we want to make this threshold variable?

One way of the many different ways to achieve this is to replace the hardcoded value in our rule with a global variable that can be defined whenever we want to run our session, as follows:

global Double amountThreshold;

rule "Detect suspicious amount operations"
when
    $c: Customer()
    Number( doubleValue > amountThreshold ) from accumulate (
        Order ( customer == $c, state != OrderState.COMPLETED, $total: total),
        sum($total)
    )
then
    insert(new SuspiciousOperation($c, SuspiciousOperation.Type.SUSPICIOUS_AMOUNT));
end

In the preceding example, we can see how the hardcoded threshold is no longer present in the DRL. We are now using a global of the Double type in the condition of our rule. Using this approach, the threshold of what we consider a suspicious operation can now be modified among the different executions of the session.

Tip

There is nothing that prevents us from modifying our global variables during the execution of our session from within a rule. Even if this is possible, modifying the value of a global that is being used in a constraint during runtime is not encouraged. Given the declarative nature of Drools, we can't predict what is the effect of modifying the value of a global variable in these situations.

One important thing to mention is that when global variables are used as part of a rule constraint, the global must be set before the pattern where it is being used is evaluated. To avoid race conditions, it is considered good practice to set the global variables of a session before any fact is inserted. A downside of using global variables in the constraints of our rules is that their values are not cached by Drools. Every time a global variable needs to be evaluated, its value is accessed. In large knowledge bases, this could create performance issues.

Tip

Given all the drawbacks of using globals to parameterize our rules, this pattern is not recommended. A much better approach to parameterize the conditions of our rules would be to make the parameters as facts themselves in our session and treat them as any other type of fact. The inclusion of this pattern in this book was just for the sake of completeness.

Globals as a way to introduce new information into a session in the LHS

Another common pattern related to globals is their usage as data sources for a session. Usually, this type of globals encapsulate the invocation of a service (database, in-memory map, web service, and so on) that introduces new objects into the session. This usage pattern always involves the from conditional element.

In order to demonstrate this scenario, we are going to modify the example introduced in the previous section and introduce a service call to retrieve the orders of our customers. The service will be modeled as an OrderService interface, containing a single method—getOrdersByCustomer—as shown in the following code:

public interface OrderService {
    public Collection<Order> getOrdersByCustomer(String customerId);
}

The idea here is to use this interface as a global that our rule can use to retrieve all the orders related to a customer. The final version of the DRL for this example will look similar to the following code:

global Double amountThreshold;
global OrderService orderService;

rule "Detect suspicious amount operations"
when
    $c: Customer()
    Number( doubleValue > amountThreshold ) from accumulate (Order ( state != OrderState.COMPLETED, $total: total) from         orderService.getOrdersByCustomer($c.customerId),sum($total))
then
    insert(new SuspiciousOperation($c, SuspiciousOperation.Type.SUSPICIOUS_AMOUNT));
end

In this version of our example, we are still using a global to hold the threshold of what we consider a suspicious operation, but we now also have a new global called orderService. Our rule is now invoking the global's getOrdersByCustomer method to get all the orders for a particular customer instead of getting the orders from the customer's orders property.

In this simple example, we may not realize the advantage of this approach—the orders of a customer are now being fetched only when/if required. In the previous version of the rule, we had to prefetch all the orders for all the customers before inserting them into the session. We didn't know, at the insertion time, whether the session will actually require all the orders for all the customers or not.

As mentioned earlier, we need to remember to set the value of the orderService global before we insert any Customer into the session, as follows:

OrderService orderServiceImpl = new OrderServiceImpl();
//a concrete implementation of OrderService.
ksession.setGlobal("orderService", orderServiceImpl);
ksession.insert(customer1);
ksession.insert(customer2);
ksession.fireAllRules();

One important thing to notice in the previous code is that we are no longer inserting the Orders as facts. The Orders will be retrieved on demand by the rules themselves. There is a catch though, a condition of a rule could be evaluated multiple times while rules are being executed. Every time the rule is re-evaluated, the data source will be invoked. When using this kind of pattern, the latency of the data source has to be taken into account.

We saw how to use a global as an interface to an external system in order to retrieve and introduce (but not insert) new information into the session. The question now is how to extract the generated SuspiciousOperation objects out of the session?

Globals as a way to collect information from a session

The rule from the previous example inserted a SuspiciousOperation object for each suspicious operation found. The problem is that these facts are not accessible from outside the session. A common pattern to extract information from a session is by using globals.

The idea behind this pattern is to use a global variable to collect the information we want to extract from the session. As the global is accessible from outside the session, any fact, object, or value that it references will also be accessible. The most common class of this type of globals is any instance of java.util.Collection or java. util.Map.

We are now going to modify the knowledge base we used in the previous section by adding a new rule that will collect any SuspiciousOperation fact into a global set:

global Double amountThreshold;
global OrderService orderService;
global Set results;

rule "Detect suspicious amount operations"
when
    $c: Customer()
    Number( doubleValue > amountThreshold ) from accumulate (Order ( state != OrderState.COMPLETED, $total: total) from orderService.getOrdersByCustomer($c.customerId),sum($total))
then
    insert(new SuspiciousOperation($c, SuspiciousOperation.Type.SUSPICIOUS_AMOUNT));
end

rule "Collect results"
when
    $so: SuspiciousOperation()
then
    results.add(
$so);
end

The code shows that we now have a new global called results and a new rule that will collect any instance of the SuspiciousOperation class into it.

The relevant Java code to execute this new version of the example is shown in the following:

Set<SuspiciousOperation> results = new HashSet<>();
ksession.setGlobal("results", results);

ksession.insert(customer1);
ksession.insert(customer2);
ksession.fireAllRules();

//variable 'results' now holds all the generated SuspiciousOperation objects.

After the rules are executed, the global set will contain the references of any SuspiciousOperation object generated during the session's execution. We can then use these objects outside the session where they were created.

Globals as a way to interact with external systems in the RHS

The last common usage pattern regarding globals that we are going to cover is the usage of globals on the right-hand side of a rule as a way to interact with an external system. The idea behind this pattern is simple, we saw that we can use a global to introduce new information into a pattern (using the from conditional element). We can also use a global to interact with external systems on the right-hand side of a rule. The interaction with this external system could be unidirectional (getting information from the system or sending information to the system) or bidirectional (sending and receiving information from the system).

Continuing the previous example, let's say that now we want to notify each SuspiciousOperation found to an external audit system. We have two options here, we now know that we can access these generated facts using the global set introduced in the previous section. We could, from within the Java code, iterate over this list and send each of its elements to the audit system. Another option is to leverage this in the session itself.

This new interface will be represented in our code by an interface called AuditService. This interface will define a single method—notifySuspiciousOperation—as shown in the following code:

public interface AuditService {
    public void notifySuspiciousOperation(SuspiciousOperation operation);
}

We need to add an instance of this interface as a global and create either a new rule that invokes its notifySuspiciousOperation method or modify the Collect results rule so that it now invokes this method too. Let's take the first approach and add a new rule to notify the audit system:

...
global AuditService auditService;
...
rule "Send Suspicious Operation to Audit Service"
when
    $so: SuspiciousOperation()
then
    auditService.notifySuspiciousOperati
on($so);
end

In the preceding code snippet, we are only showing the new code that we have introduced in the previous example. The new rule we have created is using the new global that we defined to notify the audit system about each generated SuspiciousOperation objects. It is important to remember that Drools will always execute the rules in a single thread. Ideally, the RHS of our rules should not involve blocking operations. In the case where blocking operations are required, the introduction of an asynchronous mechanism to execute the blocking operation in a separate thread is considered as a good option most of the time.

We have covered the four common usage patterns of globals in Drools. We are now going to introduce a similar concept: channels.

Channels

A channel is a standardized way to transmit data from within a session to the external world. A channel can be used exactly for what we discussed in the previous section: globals as a way to interact with external systems in the RHS. Instead of using a global, we can accomplish the same task by using a channel.

Technically, Channel is a Java interface with a single method—void send(Object object)—as shown in the following:

public interface Channel {    
    void send(Object object);
}

Channels can only be used in the RHS of our rules as a way to send data to outside the session. Before we can use a channel, we need to register it in our session. The KieSession class provides the following three methods to deal with channels:

Method

Description

void registerChannel(String name,Channel channel)

This method is used to register a channel in the session. When a channel is registered, a name must be provided. This name is then used by the session to identify the channel.

void unregisterChannel(String name)

This is the counterpart of registerChannel and it is used to unregister a previously registered channel. The name parameter passed to this method is the same name used during the registration.

Map< String, Channel> getChannels()

This method can be used to retrieve any previously registered channel. The key of the returned Map corresponds to the name that was used during the channel registration.

In the RHS of a rule, whenever we want to interact with a channel, we can obtain a reference to it through the predefined channels RHS variable. This variable provides an interface similar to a map that allows us to reference a specific channel by its name. For example, if we have registered a channel with the notifications name, we can interact with it using the following code snippet in the RHS of our rules:

channels["notifications"].send(new Object());

Concrete implementations of the Channel interface can be used to route data to external systems, notify about events, and so no. Just remember that a channel represents a unidirectional way to transmit data: the send()method in the Channel interface returns void.

Let's refactor the example from the previous section to make use of a channel instead of a global to notify an audit system about suspicious operations.

The first thing we need to do is to get rid of the auditService that we had in our knowledge base. The whole point of this example is to replace this global with a channel. Then, we need to replace the RHS from the "Send Suspicious Operation to Audit Service" rule so that it makes use of a channel instead of the old global:

rule "Send Suspicious Operation to Audit Channel"
when
    $so: SuspiciousOperation()
then
    channels["audit-channel"].send($so);
end

Now, before we can execute a session based on this knowledge base, we need to register a new channel in the session with the audit-channel name. In order to do so, we can use the registerChannel method we have already covered, as follows:

ksession.registerChannel("audit-channel", auditChannel);

In this case, the auditChannel object is an implementation of the Channel interface.

As we can see, a channel provides a more rigid, but well-defined, contract than a global. Just like with globals, we can use different implementations of a channel to provide different runtime behaviors in our rules.

One of the advantages of channels is the versatility they provide due to the fact that they are indexed using a String key. The key of a channel could be determined in runtime either in the LHS as a binding or in the RHS of a rule. This gives us more flexibility than plain variables, where the name of the variable we want to use is fixed in the DRL.

Let's move to a much more flexible way to extract information from within a session: queries.

Queries

A query, in Drools, can be seen as a regular rule without its right-hand side section. A major difference between a query and a rule is that the former may take arguments. With queries, we can use all the power of Drools' pattern-matching syntax to extract information from within a session. During runtime, we can execute a query and do whatever we want with its results. In some way, a query is a rule with a dynamic right-hand side.

Note

A query can also be used as a regular pattern inside a rule. This is the foundation of Drools' backward chaining reasoning capability. This section is only focused on queries as a way to extract information from a session. Queries used as patterns are covered in Chapter 9, Introduction to PHREAK.

Continuing with the example we were using before, let's now create a query to extract all the generated SuspiciousOperation facts from the session. The query required to do this looks similar to the following one:

query "Get All Suspicious Operations"
    $so: SuspiciousOperation()
end

As we can see, the query we have created looks exactly like a rule without its right-hand side. If we are interested in a particular customer, we can define another query that takes the customer ID as a parameter and filters all their related SuspiciousOperation objects, as follows:

query "Get Customer Suspicious Operations" (String $customerId)
    $so: SuspiciousOperation(customer.customerId == $customerId)
end

The arguments of a query are defined like the parameters of a method in a Java class: each argument has a type and a name.

There are two ways to execute a query from outside a session: on-demand queries and live queries. Let's analyze them in more detail.

On-demand queries

A query is evaluated on-demand by invoking KieSession's getQueryResults method:

public QueryResults getQueryResults(String query, Object... arguments);

This method takes the name of the query and the list of its arguments (if any). The order of the arguments corresponds to the order of the parameters in the query definition. The result of this method is a QueryResults object:

public interface QueryResults extends Iterable<QueryResultsRow> {
    String[] getIdentifiers();    
    Iterator<QueryResultsRow> iterator();    
    int size();
}

The QueryResults interface extends Iterable and represents a collection of QueryResultsRow objects. The getIdentifiers() method returns an array of the query's identifiers. Any bound variable defined in the query will became an identifier in its result. For example, our Get All Suspicious Operations query only defined one identifier, $so. Identifiers are used to retrieve the concrete value of a bound variable when a query is executed.

The following code can be used to execute the Get All Suspicious Operations query:

QueryResults queryResults = ksession.getQueryResults("Get All Suspicious Operations");
for (QueryResultsRow queryResult : queryResults) {
    SuspiciousOperation so = (SuspiciousOperation)queryResult.get("$so");
    //do whatever we want with so
    //...
}

The preceding code executes the Get All Suspicious Operations query and then iterates over the results extracting the value of the $so identifier, in this case, instances of the SuspiciousOperation class.

Live queries

On-demand queries are used when we want to execute a particular query at a particular point in time. Drools also provides another way to execute queries, it allows us to attach a listener to a query so that we can be notified about the results as soon as they become available.

Live queries are executed using the following Kie Session's method:

public LiveQuery openLiveQuery(String query,Object[] arguments,ViewChangedEventListener listener);

Just like with on-demand queries, the first argument we need to pass to this method is the name of the query we want to attach a listener to. The second parameter is the array of arguments that the query is expecting to receive. The third parameter is the actual listener that we want to attach to the query. The result of this method is a LiveQuery class instance.

Let's take a closer look at the ViewChangedEventListener interface:

public interface ViewChangedEventListener {
    public void rowInserted(Row row);
    public void rowDeleted(Row row);
    public void rowUpdated(Row row);
}

As we can see, the ViewChangedEventListener interface is used not only for receiving new facts matching the specified query, but we can also detect modifications or retractions of these facts. Drools engine will notify this listener as soon as a fact matches the specified query, when a previously matching fact gets modified or the modifications of a previously matching fact excludes it from the query's filter.

In the previous section, we saw how to use global variables to communicate the results, actions, and general rule execution information to the outside world. However, what if we wanted to do so in a generic way—for every rule—without modifying existing rules? To do so, we have other mechanisms such as Event Listeners.

Event Listeners

Drools framework provides the users a mechanism to attach event listeners into two of its main components: Kie Bases and Kie Sessions.

Events from a Kie Base are related to the structure of the packages it contains. Using org.kie.api.event.kiebase.KieBaseEventListener, for example, we can be notified after or before a package is added or removed from a KieBase. Using this same event listener, we can go deeper into detail about what is actually being modified inside a KieBase, such as individual rules, functions, and processes being added/removed.

A KieBaseEventListener can be attached to a KieBase using KieBase public void addEventListener(KieBaseEventListener listener) method. A KieBase could have none, one, or more event listeners attached to it. When a particular event has to be fired, the KieBase will sequentially execute the corresponding method in each of the previously registered event listeners. The order of execution of the listener doesn't necessarily corresponds to the order they were registered.

Kie Session's events are related to the Drools' runtime execution. The events that a Kie Session could fire are separated into three different categories: rules execution runtime (org.kie.api.event.rule.RuleRuntimeEventListener), agenda-related events (org.kie.api.event.rule.AgendaEventListener), and processes execution runtime (org.kie.api.event.process.ProcessEventListener).

All these three types of event listeners can be attached into a Kie Session using one of KieSession addEventListener methods:

public void addEventListener(RuleRuntimeEventListener listener)
public void addEventListener(AgendaEventListener listener)
public void addEventListener(ProcessEventListener listener)

A RuleRuntimeEventListener can be used to notify about the events that are related to the state of the facts inside a Kie Session. The state of the facts inside a Kie Session is modified when they are inserted, modified, or retracted from the session. This kind of listener is usually used for reporting or statistical analysis of the execution of the session.

The AgendaEventListener is the interface we could use to be notified about the events happening inside Drools' agenda. Agenda events are related to rules match being created, canceled, or fired; agenda groups being pushed or popped from the active agenda stack and about rule-flow groups being activated and deactivated. The AgendaEventListeners are a fundamental aid for auditing tools. Being able to know when a rule gets activated, for example, is a valuable piece of information when analyzing the execution of a Kie Session.

The ProcessEventListeners are related to jBPM events and allow us to be notified when a process instance is started or completed or before/after the individual nodes inside a process instance are triggered.

A more declarative way to configure the event listeners that we want to use in a session is to define them inside the kmodule.xml file as part of a <ksession> component:

<kmodule xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://jboss.org/kie/6.0.0/kmodule">
  <kbase name="KBase1" default="true" packages="org.domain">
    <ksession name="ksession1" type="stateful">
       <ruleRuntimeEventListener type="org.domain.RuleRuntimeListener"/>
       <agendaEventListener type="org.domain.FirstAgendaListener"/>
       <processEventListener type="org.domain.ProcessListener"/>
    </ksession>
  </kbase>
</kmodule>

Note

All the event listeners in Drools are executed in the same thread where the Drools framework is running. This behavior has two implications: the event listeners should be lightweight and fast and they should never throw an exception. Event listeners that perform heavy processing tasks—or even worst, blocking tasks—should be avoided, if possible. When an event is fired, the Drools execution will not continue until all the registered listeners for such events are completed. The execution time of an action in Drools that may fire events is the sum of the execution time of the task itself, plus the execution time of each individual event listener that is fired. Drools current implementation not only executes the event listeners in the same thread where it is running, but it doesn't take any precaution when an event listener is fired either. An event listener throwing an exception will break the execution of the underlying action that was being executed. Catching any possible exception in an event listener is then mandatory if we don't want to interfere with the Drools execution when something goes wrong in our listener.

In the code bundle of this chapter, there are some unit tests showing how listeners are registered and used in Drools. We strongly recommend the reader to take a look at these tests and run, debug, and even enhance them in order to get a better understanding of Drools' event listeners capabilities.

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

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