Chapter 16. Using Business Rules to Implement Services

We have looked at how we can use the rules engine to define business rules, which can then be invoked as decision points within a BPEL process. The examples we have used so far have been pretty trivial; however, the rules engine uses the Rete Algorithm, which was developed by researchers into Artificial Intelligence in the 1970s.

Rete has some unique qualities when compared to more procedural-based languages such as PL/SQL, C, C++, or Java, making it ideal for evaluating a large number of interdependent rules and facts.

This not only makes it simpler to implement highly complex rules than would typically be the case with more procedural based languages, but also makes it suitable for implementing particular categories of first class business services.

In this chapter, we look in more detail at how the rule engine works. Once armed with this knowledge, we write a set of rules to implement the auction algorithm, responsible for determining the winning bid according to the rules set out in Chapter 9oBay Introduction.

How the rule engine works

So far we have only dealt with very simple rules that deal with a single fact. Before we look at a more complicated ruleset that deals with multiple facts it's worth taking some time to gain a better understanding of the inner workings of the rules engine.

The first thing to take into account is that when we invoke a ruleset, we do it through a rules session managed by the decision service. When using the decision service, it first asserts the facts passed in by the BPEL process. Next, it executes the ruleset against those facts, before finally retrieving the result from the rule sessions.

Asserting facts

The first step is for the decision service to assert all the facts passed by the BPEL process into the working memory of the rules sessions, ready for evaluation by the rules engine.

When defining the decision service, it's important to check the box Check here to assert all descendants from the top level element. Otherwise, only the top level XML element will be asserted as a fact.

Once the facts have been asserted into working memory, the next step is to execute the ruleset.

Executing the ruleset

Recall that a ruleset consists of one or more rules, and that each rule consists of two parts: a rule condition, which is composed of a series of one or more tests, and an action-block or list of actions to be carried out when the rule condition evaluates to true for a particular fact or combination of facts.

It's important to understand that the execution of the rule condition and its corresponding action block are carried out at two very distinct phases within the execution of the ruleset.

Rule activation

During the first phase, the rules engine will test the rule condition of all rules to determine which facts or combination of facts the rule conditions evaluate to true. A group of facts that together cause a given rule condition to evaluate to true, is known as a fact set row, with a fact set being a collection of all fact set rows that evaluate to true for a given rule.

In many ways it's similar to the concept of executing the rule condition as a query over the facts in working memory, with every row returned by the query equivalent to a fact set row, and the entire result set being equivalent to the fact set.

For each fact set row, the rules engine will activate the rule. This involves adding each fact set row with a reference to the corresponding rule to the agenda of rules which need to be fired. At this point, the action block of any rule has not been executed.

When rule activations are placed on the rule agenda, they are ordered based on the priority of the rule, with those rules with a higher priority placed at the top of the agenda.

When there are multiple activations with the same priority, the most recently added activation is the next rule to fire. However, it's quite common for multiple activations to be added to the ruleset at the same time; the ordering of these activations is not specified.

Rule firing

Once all rule conditions have been evaluated, then the rule engine will start to process the agenda. It will take the rule activation at the top of the agenda and execute the action block for the fact set row and the corresponding rule.

During executing of the action block, the rule may assert new facts, assert updated facts, or retract exiting facts from the working memory. As the rule engine does this, it may cause existing activations to be removed from the agenda, or may add new activations to the agenda.

When an activation is added to the agenda, it will be inserted into the agenda based on the priority of the rule. If there are already previous activations on the agenda with the same priority, the new activation will be inserted in front of these activations. This means that the set of new activations will be processed before any of the older activations with the same priority, but after any activation with a higher priority.

Note

If a rule asserts a fact that is mentioned in its rule condition, and the rule condition is still true, then a new activation for the same fact set row will be added back to the agenda. So the rule will be fired again. This can result in a rule continually firing itself and thus the ruleset never completing.

Once the rule engine has completed the execution of the action block for an activation, it will take the next activation from the agenda and process that. Once all activations on the agenda have been processed then the rule engine has completed execution of the ruleset.

Retrieve result

Once the ruleset has completed, the decision service will query the working memory of the rule session for the result, specifically the facts that we configured the decision service to watch, which the decision service will then return to the BPEL process.

Note, for each fact that we have configured the decision service to watch, we should ensure that just a single fact of that type will reside within the working memory of the decision service upon completion of executing the ruleset. If zero or multiple facts exist, then the decision service will return an exception to the BPEL process.

Session management

Before executing a ruleset, the decision service must first obtain a rule session. Creating a rule session involves creating a RuleSession object and loading the required repository, which has significant overhead. Instead of creating a new RuleSession to handle each request, the decision service maintains a pool of shared objects that it uses to service requests.

When we invoke a decision service within a BPEL process, the decision service will allocate RuleSession object from this pool to handle the request.

In most scenarios, we will choose to assert the facts, execute a ruleset and retrieve the result within a single operation. At the end of this, the final step is to reset the session, so that it can be returned to the pool of RuleSession objects and reused to handle future requests. This pattern of invocation is known as a stateless request, as the state of the session is not maintained between operations.

However, the decision service also supports a stateful invocation pattern, which enables you to split these steps across multiple operations when more flexibility is required.

For example, you can assert some facts within the first invocation, execute the ruleset and retrieve the results (without resetting the session). Based on the result, you may then take one of multiple paths within your BPEL process. At which point you may re-invoke the decision service, asserting some additional facts, re-execute the ruleset, retrieve an updated result, and then reset the rule session.

However, stateful sessions should be used with care as the state of the rule session is not persisted as part of the dehydration of a BPEL process, so won't survive a server shutdown.

Debugging a ruleset

Because the order in which rules and facts are evaluated are not specified for rules with equal priority, when you don't get the result you are expecting it can potentially be quite hard to debug. In these situations it can be extremely useful to see what facts are being asserted, the activations that are being generated and the rules as they are being fired.

The decision service can be configured to log these events, by specifying the following properties:

  • watchFacts: Logs information about each fact as it is asserted, retracted, or modified within the working memory of a ruleset. As each fact is asserted, it is given a numeric identifier prefixed with f-, which uniquely identifies that fact within the rule session.

  • watchActivations: Logs information about each rule activation as it's placed on the agenda, including details of the facts in the row fact set for the activation.

  • watchRules: Logs information about each rule as it fires, detailing the rule fired as well as the facts in the row fact set causing the rule to fire.

These properties must configured by adding them to the decisonservices.xml file shown as follows:

<?xml version = '1.0' encoding = 'UTF-8'?>
<decisionServices xmlns="http://xmlns.oracle.com/bpel/rules">
<ruleEngineProvider name="obay.ob1" provider="Oracle">
<repository type="File">
<file>repositoryresource:obay.obr</file>
</repository>
 <properties>
<property name="watchRules">true</property>
<property name="watchActivations">true</property>
<property name="watchFacts">true</property>
</properties>

</ruleEngineProvider>
...
</decisionServices>

This file isn't available within JDeveloper as part of the BPEL project. Hence it needs to be manually opened and modified using JDeveloper or a text editor. The file can be found in the following directory

<Project Dir>decisionservicesAuctionServicewarWEB-INFclasses

Here, <Project Dir> is the home directory of the BPEL project using the decision service. Once you have set these properties you still have to configure the BPEL domain to log these events, by setting the logger default.collaxa.cube.services to debug.

Using DM.println to add additional logging

Even with the above logging information, it can be useful to produce more fine grain logging within your ruleset. You can do this using the DM.println function within your ruleset.

This function can be used either within your own functions or called as part of the action block for a rule. Again to enable these statements to be written to the BPEL domain log, you need to set the logger default.collaxa.cube.services to debug.

Using business rules to implement an auction

A good candidate for a service to implement as a ruleset is the oBay auction service. You may recall that we looked at the oBay auction process in Chapter 14 ; what we didn't cover in this chapter is the actual implementation of how we calculate the winning bid.

In this scenario our facts consist of the item up for auction and a list of bids which have been submitted against the item. So we need to implement a set of rules to be applied against these bids in order to determine the winning bid.

Defining our XML Facts

The first step in implementing our ruleset is to define our XML Facts; we can create these using the auction.xsd that we defined as part of our canonical model for oBay, shown as follows:

<?xml version="1.0" encoding="windows-1252"?>
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns="http://schema.packtpub.com/obay/auc"
targetNamespace="http://schema.packtpub.com/obay/auc"
elementFormDefault="qualified" >
 <xsd:element name="auctionItem" type="tAuctionItem"/>
<xsd:element name="bids" type="tBids"/>

<xsd:element name="bid" type="tBid"/>
<xsd:complexType name="tAuctionItem">
<xsd:sequence>
<xsd:element name="auctionType" type="xsd:string"/>
<xsd:element name="startTime" type="xsd:dateTime" />
<xsd:element name="endTime" type="xsd:dateTime" />
<xsd:element name="startingPrice" type="xsd:double" />
<xsd:element name="reservePrice" type="xsd:double"/>
<xsd:element name="winningPrice" type="xsd:double"/>
<xsd:element name="winningBid" minOccurs="0" type="tBid"/>
<xsd:element name="bidHistory" type="tBids"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="tBids">
<xsd:sequence>
<xsd:element name="bid" type="tBid" minOccurs="0"
maxOccurs="unbounded" />
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="tBid">
<xsd:sequence>
<xsd:element name="bidId" type="xsd:string" />
<xsd:element name="bidderId" type="xsd:string" />
<xsd:element name="bidtime" type="xsd:dateTime"/>
<xsd:element name="maxAmount" type="xsd:double"/>
<xsd:element name="bidAmount" type="xsd:double"/>
<xsd:element name="status" type="xsd:string"/>
</xsd:sequence>
</xsd:complexType>
</xsd:schema>

Examining this we can see that this maps nicely to facts that we have already identified. We have the element auctionItem which maps to our auction fact.

This has a start and end time during which bids can be received, a starting price and a reserve price (which defaults to the starting price if not specified). It also contains an optional winning bid element, which holds details of the current winning bid for the auction (if there is one) as well the bid history element, which contains details of all failed bids.

When we first create an auction, we won't have received any bids. So initially our auctionItem will not contain a winning bid and the bid history will be empty, as in the following example:

<auctionItem>
<auctionType>STD</auctionType>
<startTime>2008-09-01T15:45:48 </startTime>
<endTime>2008-09-08T15:45:48</endTime>
<startingPrice>1.00</startingPrice>
<reservePrice>5.00</reservePrice>
<winningPrice>0.00</winningPrice>
<bidHistory/>
</auctionItem>

Against this we need to apply one or more bids; this is contained within the fact bids, which contains one or more bid elements of type tBid.

Note

As part of the auction process, as each bid is submitted to the BPEL process, it will assign a unique ID to the bid (within the context of the auction), set the bidtime to the current time and set the status of the bid to NEW, before submitting it to the Auction ruleset.

So, for example, if we submitted the following set of bids against the above item:

<bids>
<bid>
<bidId>1</bidId>
<bidderId>jcooper</bidderId>
<bidtime>2008-09-06T12:27:14</bidtime>
<maxAmount>12.00</maxAmount>
<bidAmount>0.00</bidAmount>
<status>NEW</status>
</bid>
<bid>
<bidId>2</bidId>
<bidderId>istone</bidderId>
<bidtime>2008-09-07T10:15:33</bidtime>
<maxAmount>10.00</maxAmount>
<bidAmount>0.00</bidAmount>
<status>NEW</status>
</bid>
</bids>

we would want the rule engine to return as an updated auctionItem fact that looked like the following.

<auctionItem>
<auctionType>STD</auctionType>
<startTime>2008-09-01T15:45:48 </startTime>
<endTime>2008-09-08T15:45:48</endTime>
<startingPrice>1.00</startingPrice>
<reservePrice>5.00</reservePrice>
<winningPrice>10.50</winningPrice>
<winningbid>
<bidId>1</bidId>
<bidderId>jcooper</bidderId>
<bidtime>2008-09-06T12:27:14</bidtime>
<maxAmount>12.00</maxAmount>
<bidAmount>10.50</bidAmount>
<status>WINNING</status>
</winningbid>
<bidHistory>
<bid>
<bidId>2</bidId>
<bidderId>istone</bidderId>
<bidtime>2008-09-07T10:15:33</bidtime>
<maxAmount>10.00</maxAmount>
<bidAmount>10.00</bidAmount>
<status>OUTBID</status>
</bid>
</bidHistory>
</auctionItem>

Defining the decision service

Once we have created our dictionary containing our XML facts, we can create an empty ruleset (called Auction in our example). At this point we can already create a decision service to invoke the ruleset.

For the Auction Decision Service we need to pass in two facts: AuctionItem and Bids and return the single fact AuctionItem as shown in the following screenshot:

Defining the decision service

At this point we can actually save and run the ruleset from our Auction Process. Assuming everything works as expected, it will return a result containing details of the actual auction item that we passed in. All that remains now is for us to write the rules to evaluate our list of bids.

Using a global variable to reference the result set

When we configure a decision service, we specify one or more facts that we want the decision service to watch (that is, AuctionItem in the previous example); these are often referred to as the result set.

Many of our rules within the ruleset will require us to update the result set. For example, every time we evaluate a bid, we will need to update the AuctionItem fact accordingly, either to record a bid as the new winning bid or add it to the bid history as a failed bid.

When a rule is fired, the action block is only able to operate on those facts contained within its local scope, which are those facts contained in the fact set row for that activation. Or put more simply, the rule can only execute actions against those facts which triggered the rule.

This means that for any rule which needs to operate on the result set, we would need to include the appropriate test within the rule condition in order to pull that fact into the fact set row for the activation. So, in the case of our Auction ruleset, we would need to add the following statement to every rule which needed to operate on the AuctionItem fact:

AuctionItem is a AuctionItem

This just adds an extra level of complexity to all our rules, particularly if you have multiple facts contained within the result set. It's considered better practice to define a global variable which references the result set, which we can access within the action block of any rule and within any function we define.

Defining a global variable

To create a global variable, from within the Definitions tab, select the Variables folder. This will bring up the Variables Summary, which lists all the variables currently defined to our ruleset. Click Create to bring up the Variable editor page as shown in the screenshot.

Here we have defined a variable of type AuctionItem and given it a corresponding name and alias. For the purpose of clarity, we tend to prefix all variables with var to indicate that it's a global variable.

If we check the box Final the variable is fixed, that is, it becomes a constant, which can then be used within the test part of a rule. However, as we want to be able to update the variable we have left this unchecked.

Defining a global variable

Finally, we can define an expression to initialize the variable. With XML facts you would often call a function to create the fact and initialize the variable. In our case, we want to initialize it to reference the AuctionItem fact passed in by the decision service.

Since variables are created and initialized prior to asserting any facts, we will need to define a rule to do this once AuctionItem has been asserted. So here we are just setting our variable to null.

Defining a rule to initialize a global variable

As you can see from the following diagram, the rule to initialize our global variable is pretty straightforward.

Defining a rule to initialize a global variable

The key point worth noting is that we have specified a priority of 100 (the default is 0) for the rule. This is to ensure that this rule is fired before any of the other rules which reference this variable.

Writing our auction rules

The next step is to write the rules to determine the winning bid. We could write a very simple rule to find the highest bid by writing a rule condition statement such as the following:

winningBid is a TBid
There is no case where otherBid is a TBid and
otherBid.maxAmount > winningBid.maxAmount

This will match the bid which has no other bids with a greater bid amount. However, if we examine the bidding rules of an auction, we can see that the highest bid doesn't always win.

The reason being that once a successful bid has been placed, the next bid has to be equal to the winning amount plus a full bid increment; otherwise it's not a valid bid. In addition if two maximum bids are equal, then the bid that was placed first is deemed the winning bid.

Evaluating facts in date order

In other words we need to evaluate our bids in date order, the earliest first, and then the next, and so on. Once a bid has been processed, its status will be set to WINNING, OUTBID, or INVALID as appropriate.

So we need to write a rule to select a bid with a status of NEW which has an earlier bidtime than any other bid with a status of NEW. This can then be evaluated against our auction rules to determine its success or otherwise.

The first part of the rule condition is straight forward; we just need to implement a pattern such as:

nextBid is a TBid and
nextBid.status == "NEW"

This will of course match all bids with a status of NEW.

Checking for non-existent fact

So we need to define a second pattern that checks to see if no other bids exist (with a status of NEW) with an earlier bid time; in other words we have to check for the non-existence of a fact.

We do this by defining a pattern of type There is no case which will fire once if there are no matches, that is, no earlier bids. So our extended rule condition is implemented as follows:

nextBid is a TBid and
nextBid.status == "NEW"
There is no case where anotherBid is a TBid and
anotherBid.status == "NEW" &&
anotherBid.bidtime.before(nextBid.bidtime)

This condition works as follows; the first test will select all the bids with a status of NEW. For each bid selected it will execute the second test where it will select all other bids with a status of new and an earlier bid time; if no bids are selected then this test will evaluate to true and the rule will be activated and placed on the agenda.

When the activation is placed on the agenda, only the fact referenced by nextBid is included in the fact row set, because for the rule condition to be true, anotherBid won't actually reference any other bid.

Using Calendar functionality

You may have noted that the property bidtime, which is defined within our schema as xsd:datetime maps to a java.util.Calendar. When comparing properties of type Calendar within a rule, we can't use the standard operators (such as >, >=, <= and <) to do this.

Rather we need to use the appropriate methods (for example before, after) provided by the Calendar class. Now we could write our own functions that wrap these methods calls, or alternatively as we have done above invoke them directly within our rules.

In order to do this, we first need to import the java.util.Calendar class as a Java fact within our dictionary. Once we have done this, the rule editor won't expose the methods. Rather we need to specify that our test is an Advanced Test and manually enter the code.

Updating the bid status

Once we have located the next bid, we need to set its status to NEXT and re-assert it; we do this with the following statements in our action block.

Assign nextBid.status = "NEXT"
Assert nextBid

An interesting side effect is that as soon as we assert our modified bid, the rule engine will re-apply the test condition and potentially find another bid with a status of "NEW", that is, the next bid to be processed after this one.

On finding this bid, it will place a new activation on the agenda for this rule referencing this new bid. To prevent this rule from firing before any of the rules which process bids with a status of "NEXT", we have set the priority of this rule to 0.

So the complete rule to get the next bid is defined as follows:

Updating the bid status

Using inference

Once we have identified the next bid, we could then, within the same rule, include the logic to determine the success or otherwise of the bid. However, when processing a bid, we have to deal with the following three potential scenarios:

  1. The next bid is higher than the current winning bid.

  2. The current winning bid is higher than or equal to the next bid.

  3. This is our first bid and thus by default it is our winning bid.

Before evaluating a bid we also need to check that it's valid; specifically we must check:

  • The max bid amount is greater than or equal to the starting price of the item.

  • The max bid amount is greater than the current winning price plus one bidding increment.

If we encompassed all these checks within a single rule, we would end up with a very complex rule.

For example, to write a single rule for the first scenario, we would need to write a rule condition to identify the next bid, validate it and finally check if it is higher than the current winning bid, so would end up with a rule condition such as this:

nextBid is a TBid and
nextBid.status == "NEW"
There is no case where anotherBid is a TBid and
anotherBid.status == "NEW" &&
anotherBid.bidtime.before(nextBid.bidtime)
auctionItem is a TAuctionItem and
nextBid.maxAmount >= auctionItem.startingPrice
winningBid is a TBid and
winningBid.status == "WINNING" &&
nextBid.maxAmount >= winningBid.bidAmount +
getBidIncrement (winningBid.bidAmount)
nextBid.maxAmount > winningBid.maxAmount

We would then need to re-implement most of this logic for the other two scenarios.

Better practice is to use inference, that is, if A implies B, and B implies C, then we can infer from this that A implies C. In other words, we don't have to write this all within a single rule; the rule engine will automatically infer this for us.

In our scenario this means writing a rule to get the next bid (as covered above). Next, writing two rules to validate any bid with a status of next, these rules will retract any invalid bids and update their status to reflect this. Finally we need to write three rules, one for each of the scenarios identified above to process each valid bid.

The only thing we need to take into account is that the validation rules must have a higher priority than the rules which process the next bid. Hence, that they retract any invalid bids before they can be processed.

Processing the next valid bid

Using inference we can now write our rules to process the next bid on the basis that we already know which bid is next and that the bid is valid. Using this approach, the rule condition for the first scenario where the next bid is higher than the current winning bid, would be specified as:

nextBid is a TBid and
nextBid.status == "NEXT"
winningBid is a TBid and
winningBid.status == "WINNING" &&
winningBid.maxAmount < nextBid.maxAmount

This, as we can see, is considerably simpler than the previous example.

If this evaluates to true for our next bid, then we have a new winning bid and need to take the appropriate actions to update the affected facts as well as the result set.

The first action we need to take is to calculate the actual winning amount by adding one bidding increment to the maximum amount of the losing bid. So the first statement in our rules action block is as follows:

Assign nextBid.bidAmount = winningBid.maxAmount +
getBidIncrement (winningBid.maxAmount)

Where DM.getBidIncrement is a function that calculates the next bid increment, based on the size of the current winning amount.

Next, we need to update its status to WINNING and re-assert the bid in order that it will be re-evaluated as a winning bid by our ruleset.

In addition, we need to update the status of our previous winning bid to OUTBID and retract, if from the rule space, as we no longer need to evaluate it.

Using functions to manipulate XML Facts

As part of the process of evaluating a new winning bid, we also need to update our result set. This includes creating a new XML element of type TBid to hold the details of the losing bid and insert this into the bidHistory element as well as updating the winningBid element with details of our new winning bid.

To create new instances of XML elements we need to use the corresponding JAXB ObjectFactory class that the rule author generated when we imported the auction schema.

Rather than performing this manipulation of the XML structure directly within the action block of our rules, it's considered best practice to implement this as a function, which can then be called from our rule. This helps keep our rules simpler and more intuitive to understand.

So for the above purpose we need to define two functions assertWinningBid and retractLosingBid.

Asserting a winning bid

To record details of a new winning bid in the result set, we have defined the function DM.assertWinningBid, which takes a single parameter bid of type TBid, used to pass in a reference to the winning bid. The code for this function is as follows:

// Update Status of Winning Bid

bid.setStatus("WINNING");
assert(bid);
 // Update result set with details of Winning Bid

varAuctionItem.setWinningPrice(bid.getBidAmount());
com.packtpub.schema.obay.auction.TBid winningBid = varAuctionItem.getWinningBid();
// Create Winning Bid if one doesn't exist
if (winningBid == null)
{
com.packtpub.schema.obay.auc.ObjectFactory of =
new com.packtpub.schema.obay.auc.ObjectFactory();
winningBid = of.createTBid();
varAuctionItem.setWinningBid(winningBid);
}
winningBid.setBidAmount(bid.getBidAmount());
winningBid.setBidderId(bid.getBidderId());
winningBid.setBidId(bid.getBidId());
winningBid.setBidtime(bid.getBidtime());
winningBid.setMaxAmount(bid.getMaxAmount());
winningBid.setStatus(bid.getStatus());

Looking at this, we can see it breaks into two parts. The first part updates the status of the winning bid to 'WINNING', and asserts the bid. Now, we didn't need to include this functionality within the function, we could have achieved the same result within the rule itself be defining the following actions:

Assign nextBid.status = "WINNING"
Assert nextBid

We need to process a winning bid in multiple rules; including this in the function both simplifies our rules and ensures that we handle winning bids in a consistent way. Either approach is valid; it just comes down to personal preference.

However, to indicate to callers of the function that we are asserting the winning bid in the function, we have prefixed the name of the function with 'assert'.

The second part of the function is used to update the result set with details of the winning bid. The first line updates the element winningPrice to contain the bid amount of the winning bid.

The next set of code is more interesting. First it calls the method getWinningbid() on the result set to get a reference to the winning bid element. This may return null, as the AuctionItem may not currently have a winning bid (that is, if this is the first winning bid).

To create any new XML elements we need an appropriate ObjectFactory, so we create a new instance of one with the following line of code:

com.packtpub.schema.obay.auc.ObjectFactory of =
new com.packtpub.schema.obay.auc.ObjectFactory();

Next we use the ObjectFactory to create a new element of type TBid as follows:

winningBid = of.createTBid();

Finally we update the winning bid element in AuctionItem to point to this newly created element as follows:

varAuctionItem.setWinningBid(winningBid);

Once we've done this we update the details of the winningBid element with those of the bid element.

The final thing to note is that we are not asserting varAuctionItem or any of the elements we have added to it. Hence, none of these changes will be visible to our ruleset, which is exactly what we want. This is because we are using the result set as a place to build up the result of executing our ruleset and thus don't want it included in the evaluation.

Retracting a losing bid

To record details of a losing bid in the result set, we have followed a similar approach and defined the function DM.retractLosingBid, which takes a single parameter bid of type TBid. The code for the function is as follows:

// Update Status of Losing Bid
bid.setStatus("OUTBID");
bid.setBidAmount(bid.getMaxAmount());
retract(bid);
// Record Details of Bid in Result Set
com.packtpub.schema.obay.auc.TBid losingBid = DM.cloneTBid(bid);
java.util.List bidHistory = varAuctionItem.getBidHistory().getBid();
if (bidHistory.isEmpty()) {
bidHistory.add(losingBid);
}
else {
bidHistory.add(0,losingBid);
}

Looking at this, we can see that, as with the previous function, it breaks into two parts. The first part updates the status of the losing bid and then retracts it. The second part of the function is used to record details of losing bid within the bidHistory element of our result set.

The first line of this part calls the function DM.cloneTBid to create a new element of type TBid and initialize it with the values of the losing bid using a approach similar to the one previously used to create a new winning bid element.

Once we've done that, we then add it to the bidHistory element. The bid history itself is a collection of bid elements. JAXB implements this as a java.util.List, the method getBid returns a reference to this list.

The final part of the function inserts the losing bid at the start of this list, so that the bid history contains the most recently processed bid at the start of the list.

Rules to process a new winning bid

With our functions defined, we can finish the implementation of the rule for a new winning bid, which is shown in the following screenshot:

Rules to process a new winning bid

Due to the use of inference to simplify the rule condition and the use of functions to manipulate the result set, the final rule is very straightforward.

The only thing we need to take into account is the priority of the rule, which we have set to 50. This is to ensure that the validation rules for a bid have a higher priority so that they are fired first.

Validating the next bid

For the above rule to be complete we need to define the rules which validate the next bid before we process it; the two conditions that we need to check are:

  • The max bid amount is greater than or equal to the starting price of the item.

  • The max bid amount is greater than the current winning price plus one bidding increment.

To validate that max bid amount is greater than or equal to the auction starting price, we have defined the following rule:

Validating the next bid

The function retractInvalidBid is almost identical to the function retractLosingBid, the only difference being that it sets the status of the bid to 'INVALID'.

We have also defined a similar rule, validateBidAgainstWinningPrice to validate that the max bid amount is greater than the current winning amount plus one bidding increment.

Each of these rules has a priority of 80, which is higher than the rules for processing the next bid. This ensures that any invalid bids are retracted before they can be processed.

Rule to process a losing bid

The rules to handle the other potential outcomes for the next bid, namely where it's our first bid, and thus by default a winning bid or a losing bid, are straightforward, apart for one exception. The rule for the scenario where the next bid is a losing bid is shown here:

Rule to process a losing bid

If we look at the first action that sets the bid amount of the winning bid equal to the maximum amount of the losing bid plus the next bid increment, there is a possibility that this could cause the bid amount to exceed the maximum amount specified.

For example if the maximum bid was $10, with the current winning amount being $5, then it would be valid for the next bid to be $10. This bid would fail but the new winning amount according to the above would be $10.50.

Capping the winning bid amount

To prevent this from happening we need to write another rule to test if the winning amount of the bid is greater than its maximum amount and if it is then set the winning amount equal to the maximum amount. The rule for this is shown in the following screenshot:

Capping the winning bid amount

The rule itself is straightforward. But as this rule is being used to correct an inconsistent state we have given it a priority of 90 so that it is fired even before the validation rules.

Complete ruleset

In total we have eight rules within our Auction ruleset; these are listed below in order of priority.

Rule

Priority

InitialiseVarAuctionItem

100

CapWinningBid

90

ValidateBidAgainstStartPrice

80

ValidateBidAgainstWinningPrice

80

FirstBid

50

NewWinningBid

50

LosingBid

50

GetNextBid

0

The first rule is just used to initialize the global variable, which references the result set. The next rule, CapWinningBid, ensures that we don't breach the maximum amount for a bid. The next two rules: ValidateBidAgainstStartPrice and ValidateBidAgainstWinningPrice are just simple validation rules.

The majority of the work is done in the next three rules: FirstBid, NewWinningBid and LosingBid, each of which deals with one of the three possible outcomes each time we have to process a new bid. The final rule, GetNextBid, is used to ensure that we process each bid in date order.

Performance considerations

In the example we've been working on the basis that every time we receive a new bid we add that to our list of bids received and then submit the auction and the entire list of bids to the ruleset for evaluation.

The obvious issue with this technique is that we are re-evaluating all bids that we have received from scratch every time we receive a new bid.

One possible solution would be to have a stateful rule session. With this approach we would first submit the auction item to the decision service, but no bids. Then, as we receive a bid, we could assert that against the ruleset and get the updated result back from the decision service.

The issue with this (as we discussed at the start of this chapter) is when the BPEL process dehydrates, which in the case of our auction process will happen each time we wait for the next bid, the rule session is not persisted. Consequently, whenever the server is restarted we will lose the rules session of any auction in progress, which is clearly not desirable.

Managing state within the BPEL process

One alternative is to use the BPEL process to hold the state of the rule session. With this technique we need to ensure that all relevant facts contained within the rule session are returned within the facts that the decision service is watching.

Next time we invoke the decision service, we can re-submit these facts (along with any new facts to be evaluated) and re-assert them back into a new rule session.

In the case of our Auction ruleset, the relevant facts that need to be maintained between invocations are auctionItem and winningBid which is contained within auctionItem.

With this approach, each time we receive a new bid we just need to assert the auctionItem element as returned by the previous invocation of the ruleset and the new bid (within the bids element). As a result, each time we submit a new bid, rather than re-evaluate all bids to determine the winning bid, we just need to evaluate the new bid against the winning bid, which is clearly more efficient.

To support this, we do not have to make any modifications to our ruleset, because we have implemented it in such a way that it supports either asserting all bids in one go or submitting them incrementally.

The only remaining drawback with this approach is that the ruleset will still assert all bid objects contained within the bidHistory element of auctionItem into working memory. While this won't change the outcome, it still means all these bids will be evaluated in the process of firing the rules, though none of them will cause an activation to happen.

Where we have only a relatively small number of facts this doesn't really cause a problem, but if the number of facts is in the high hundreds or order of 1000s, then this may make a noticeable difference.

Using functions to control the assertion of facts

The reason that all facts are asserted into the working memory of the rule session is that we checked the box Check here to assert all descendants from the top level element.

This causes the function assertXPath to be called for each fact passed in by the decision service, which causes all the descendants of the fact elements to be asserted at run time.

An alternative is to leave this unchecked and write a function for each fact passed in that asserts just the desired facts. So in our case we would write a function to assert the winningBid element in auctionItem and all the bid elements contained in bids.

Summary

The Business Rules Engine is built on a powerful inference engine, which it inherits from its roots in the Rete Algorithm. We spent the first part of this chapter explaining how the rule engine evaluates facts against rules. The operation of the Rete algorithm can be a challenge to completely understand, so re-reading this section may be beneficial.

However, once you have an appreciation for how the rule engine works and can start "thinking in Rete", you have a powerful tool not just for implementing complex business rules but also a certain type of service.

We demonstrated this by developing a complete ruleset to determine the winning bid for an auction. Looking at the final list of rules, we can see that we needed relatively few to achieve the end result, and that none of these were particularly complex.

As is the case when implementing a more typical decision services, we have the added advantage that we can easily modify the rules that implement a service without having to modify the overall application, giving us an even greater degree of flexibility.

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

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