Sequential algorithm

Unlike the RetePlus algorithm, the sequential execution mode does not provide inference. The sequential algorithm works in a predictable way only when rules are homogeneous, that is, when they are using the same bindings.

To guarantee the same execution with either algorithm, both the following conditions are required:

  • The rules must be homogeneous. Homogeneous rules mean that the conditions of those rules must be set on the same kind and number of objects. If you try to apply the sequential algorithm to heterogeneous rules, it typically creates more tuple objects, which can result in more rules being run than in the RetePlus mode. The common tuple signature can also cause heterogeneous rules not to fire: When there are no corresponding instances in working memory, no tuples can be created.

  • The rules must not be chained, that is, the modification done in the action part of a rule must not lead to execution of another rule.

For the rulesets that can benefit from sequential processing mode, the sequential execution mode utilizes a bytecode generation that can significantly improve the speed of rule processing. If you specify the sequential execution mode for rules that are not compliant with it, the engine raises an error.

The use of sequential execution mode is described further in the sections:

Tuples

A tuple is a list of objects that complies with a particular structure. A tuple structure is simply a sequence of class descriptors. Each location in a tuple structure is called a slot. The same class descriptor can appear more than once at different slots.

Take a tuple structure:

(Customer,Product)

Here is a tuple that complies with this structure:

(new Customer("Henry"),new DVD("Mickey"))

Note that the subclassing is taken into account because a DVD is a Product. However, some tuples that do not comply with the structure:

For example, the following tuple is too large:


(new Customer("Henry"),new CD("Madona"),new DVD("Mickey"))

This tuple is not correctly ordered:


(new DVD("Lord of the rings"),new Customer("Henry"))

And this tuple is too small:


(new Customer("Henry"))

In Java™, a tuple is easily implemented as an array of objects:


Java Tuple = Object[]

The Java code to create a tuple and its objects is:


new Object[] {new Customer("Henry"),new DVD("Mickey")}

At run time, the tuples are built either by hand or automatically from the content of the working memory and then passed to the rule engine. The tuples are passed one after the other to the tuple-matching method.

Tuples are constructed from the objects in the working memory only if they match the tuple structure: a common signature that is shared by all rules in the sequential task. After the tuple generation, all rules are run against all tuples. Therefore, it is recommended to use homogeneous rules with the Sequential algorithm.

If you provide your own tuple, it must always match the signature, but it does not necessarily have to be complete. A partial tuple is a tuple with unused slots. Unused slots are simply set to null. For instance, the tuple(new Customer("Henry")) that was too small could be coded as (new Customer("Henry"),null) in order to fulfill the tuple structure.

For example, if you have the following two rules, the signature is [Integer, Integer]. If there is only one Integer in the working memory, rule r2 does not fire.
rule r1 {
when{
  Integer();
  Integer();
}
then {...


rule r2 {
when{
  Integer();
}
then {...
For example, if you have the following two rules, the signature is [String, Integer]. If you have two Strings (S1, S2) in the working memory and one Integer (I1), rule r2 fires once for each tuple [S1, I1] [S2, I1]. This means that r2 fires two times for I1.
rule r1 {
when{
  Integer();
  String();
}
then {...


rule r2 {
when{
  Integer();
}
then {...

Rule selection in sequential mode

The rules of a rule task that are set with the property algorithm equal to sequential are dynamically translated to Java bytecode. The bytecode is capable of doing sequential processing when the task is run.

The body property of a rule task is mandatory. It selects the rules in the ruleset that are taken into account by the rule task.

See also Runtime rule selection.

Control properties in sequential mode

Control properties affect the sequential mode execution and the tuple structure. See Control properties for rule tasks in execution modes for more general information.

The number of rules to be executed per tuple and the order in which they execute can be specified with control properties: ordering, firing, and firinglimit.

The ordering property

The ordering property is mandatory for sequential processing. It describes the order by which the rules of the rule task will be executed.

There are two values available:

  • literal: The rules are kept in the order in which they are set into the task body (using up/down arrows).

  • sorted: The rules are sorted according to their static priorities.

Note that another value, dynamic, is available only for rule tasks using the RetePlus execution mode.

The firing property

The firing property specifies whether, for each tuple, all the rules or only the first rule that applies should be executed. This property is optional.

It has two possible values:

  • allrules

    This value means that all the rules that apply should be executed before skipping to the next tuple. This is the default value.

  • rule

    This value means that only the first rule that applies should be executed on the first tuple.

The firinglimit property

The firinglimit property gives another level of control when the firing property is set to allrules. It is used to specify a number that represents the maximum number of rules to be executed before skipping to the next tuple. This number should be greater than zero.

Known limitations of the sequential algorithm

In almost all cases there is no limitation in the number of rules handled by a single task that has an algorithm property equal to sequential. The only exception is with extremely large decision tables with an otherwise. The processing is as follows: All the conditions of all the lines are negated in a single expression. This single expression becomes the body of a single method once translated to bytecode. This is an issue because the Java verifier rejects methods with too big a body. The scalability algorithm used for the rules does not go down to the method body. The user is notified of this limitation when the class loader rejects the rules. The limit in the number of rules is not clear-cut, but the number is likely to be several thousands.

The following table outlines the limitations of sequential processing, in particular its use combined with certain ILOG® Rule Language (IRL) constructs.

Limitation Description
ILOG Rule Language (IRL) limitations

Not all the rule patterns that have been designed for the stateful Rete matching are available. Compile time errors occur when a rule is not compliant with the current tuple-matching implementation.

Therefore, when sequential processing is used, the IRL does not support the following features:

  • dynamic priorities

    Because there is no agenda in sequential processing, only the rules with static priorities are allowed.

  • not, exists, collect conditions without an enumerator

    The not, exists, and collect conditions with an enumerator (from or in) are translated to Java bytecode. When an enumerator is specified, the set of objects is available as a value tied to the condition. (Note that this is not a limitation for RetePlus or Fastpath.)

Condition constructs

The following condition constructs are available in sequential processing:

  • simple condition—bindings and tests are available

  • from

  • in

Exception handling

Exception handling is not supported in rule condition parts in sequential and Fastpath modes.

For example, if you specify an exception handler on a context, it is not taken into account in sequential and Fastpath modes:

IlrContext mycontext="....";
IlrExceptionHandler myhandler="....";
mycontext.setExceptionHandler(myhandler);
Refraction

The data unit for an engine cycle is the tuple. The engine does not record the tuples as they are matched, it rather forgets them between cycles. Therefore, there is no built-in support for refraction.

Using the sequential execution mode, the above rule task becomes:


ruletask main {
   algorithm = sequential;
   ordering = literal;
   body = {Person,PersonProduct}
}

The computed tuple structure:


(Person,Product)

The generated tuple matching code:


void matchObjects(Object[] tuple) {
   rule_Person((Person)tuple[0]);
   rule_PersonProduct((Person)tuple[0],(Product)tuple[1]);
}

The main code remains the same:

  1. context.insert(new Person("Henry"));

  2. context.insert(new CD("Madona"));

  3. context.insert(new DVD("Mickey"));

  4. context.execute();

The working memory iterator loop:

  1. matcher.matchObjects((Person("Henry"),CD("Madona")));

  2. matcher.matchObjects((Person("Henry"),DVD("Mickey")));

The execution trace:

  1. // matcher.matchObjects((Person("Henry"),CD("Madona")));

  2. Person(Henry)

  3. PersonProduct(Henry,Madona)

  4. // matcher.matchObjects((Person("Henry"),DVD("Mickey")));

  5. Person(Henry)

  6. PersonProduct(Henry,Mickey)

The execution trace shows that, by using the sequential execution mode, the same rule is run twice for the same parts of two different tuples.

ILOG Rule Language (IRL) facilities

In contrast, practically all the script-level expressions and statements of the IRL are available in sequential processing. They serve principally to maintain the consistency of the other rule tasks that are not set to the sequential algorithm.

Here is a list of these features:

Functions

Function definitions and calls are fully available.

Script-level expressions

All the IRL expressions are available. (Note that ?instance works in any execution mode.)

Script-level statements

All the IRL statements are available.

Update

The update construct is available because spurious updates might exist in rule tasks set to the sequential algorithm. A spurious update is an update on an object that cannot be matched by any condition part of rule tasks set to the sequential algorithm, but might be significant for other rule tasks using the RetePlus algorithm. All rule tasks are specified in the ruleset and should be kept consistent with the current engine. However, it is important to note that the usual update is not handled at all by the stateless sequential processing algorithm.

Update refresh

The refresh mode is not handled at all by the sequential processing algorithm, but it might be relevant for other rule tasks using the RetePlus algorithm.

Insert

Insertion of objects that could match condition parts of rule tasks set to the sequential algorithm might cause inconsistencies when the tuples of objects are extracted from working memory. This is because the objects are inserted in the first position, and the iterator building the tuples is not notified. The iterator does not, therefore, give the tuples involving the new object to the sequential processing engine. However, the insert is still relevant for other rule tasks using the RetePlus algorithm.

The following examples show cases that cause Just-In-Time (JIT) compilation errors, that is, an error at execution time as opposed to a parsing error.

Example 1: IRL limitation causing compilation error

This example results in JIT compilation errors due to missing enumerators.


ruletask T {
  algorithm = sequential;
  body = { R1, R2, R3, R4, R5, R6, R7, R8, R9 }
}

rule R1 {
  when {
    not MyObject();  // No enumerator, an error will occur
  }
  then {
  }
}

rule R2 {
  when {
    exists MyObject();  // No enumerator, an error will occur
  }
  then {
  }
}

rule R3 {
  when {
    collect MyObject() where (size() == 1);  // No enumerator, an error
                                                will occur
  }
  then {
  }
}

function Object getObject() {
  return new MyObject();
}

function Object[] getObjects() {
  Object objs = new Object[1];
  objs[0] = getObject();
  return objs;
}

rule R4 {
  when {
    not MyObject() from getObject();  // An enumerator, no error
  }
  then {
  }
} 

rule R5 {
  when {
    not MyObject() in getObjects();  // An enumerator, no error
  }
  then {
  }
} 

rule R6 {
  when {
    exists MyObject() from getObject();  // An enumerator, no error
  }
  then {
  }
} 

rule R7 {
  when {
    exists MyObject() in getObjects();  // An enumerator, no error
  }
  then {
  }
} 

rule R8 {
  when {
    collect MyObject() from getObject() where (size() == 1); 
    // An enumerator, no error
  }
  then {
  }
} 

rule R9 {
  when {
    collect MyObject() in getObjects() where (size() == 1);  
    // An enumerator, no error
  }
  then {
  }
}

Example 2: IRL showing construct limitations

Here is an example with a collect construct to demonstrate the scope of the bindings. This holds for not and exists too. This example also results in JIT compilation errors due to the use of unsupported constructs.


rule R10 {
  when {
    r:MyRootObject();
    c:collect MyObject(n:name; n.startsWith("ilog")) in r.getObjects(); 
    where (s:size(); s > 1);
  }
  then {
    out.println(r);  // correct
    out.println(c);  // correct
    out.println(s);  // correct
    out.println(n);  // error
  }
}