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 9 — oBay Introduction.
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.
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.
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.
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.
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.
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.
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.
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.
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
.
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
.
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.
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
.
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>
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:
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.
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.
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.
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.
As you can see from the following diagram, the rule to initialize our global variable is pretty straightforward.
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.
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.
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
.
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.
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.
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:
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:
The next bid is higher than the current winning bid.
The current winning bid is higher than or equal to the next bid.
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.
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.
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
.
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.
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.
With our functions defined, we can finish the implementation of the rule for a new winning bid, which is shown in the following screenshot:
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.
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:
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.
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:
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.
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:
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.
In total we have eight rules within our Auction ruleset; these are listed below in order of priority.
Rule |
Priority |
---|---|
|
100 |
|
90 |
|
80 |
|
80 |
|
50 |
|
50 |
|
50 |
|
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.
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.
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.
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
.
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.
3.144.37.38