We are working on a resource reservation system where we will get requests to book a resource for any arbitrary time (ranging from 5 mins - 2 hours). There are no predefined slots.
We trying to use Drools (Fusion) ver. 7.12.0.Final, to write the rules that will ensure that new booking requests do not override existing request that have been confirmed.
To achieve the above we have created the following 2 objects :
declare ReservationRequest
#role ( event )
#duration (reservationDuration )
#timestamp ( pickupTime )
id : int
pickupTime : Date
reservationDuration : long
reserved : String
deliveryEnd : Date
end
And
declare Reservation
reservation : ReservationRequest
end
'ReservationRequest' are being treated as events so that we can use temporal operators to evaluate new requests do not overlap confirmed requests (captured as an inferred 'Reservation' fact.
We have written the following rule to check for the overlap and insert the corresponding 'Reservation' fact:
rule "Ensure no overlaped reservations using inferred facts"
enabled true
when
reservationRequest : ReservationRequest( )
not Reservation ( reservation == reservationRequest )
reservations : Reservation()
not ReservationRequest( this overlaps reservations.reservation )
not ReservationRequest( this overlappedby reservations.reservation )
not ReservationRequest( this includes reservations.reservation )
then
insert( new Reservation ( reservationRequest ) );
end
Unfortunately the above rule fails to run if there is a 'Reservation' fact in WM. I get the following error :
java.lang.RuntimeException: Conversion to long not supported from com.sample.ReservationRequest
at org.drools.core.base.extractors.BaseObjectClassFieldReader.getLongValue(BaseObjectClassFieldReader.java:133)
at org.drools.core.base.ClassFieldReader.getLongValue(ClassFieldReader.java:198)
at org.drools.core.rule.VariableRestriction$TemporalVariableContextEntry.updateFromTuple(VariableRestriction.java:641)
at org.drools.core.common.SingleBetaConstraints.updateFromTuple(SingleBetaConstraints.java:116)
at org.drools.core.phreak.PhreakNotNode.doLeftInserts(PhreakNotNode.java:108)
at org.drools.core.phreak.PhreakNotNode.doNode(PhreakNotNode.java:85)
at org.drools.core.phreak.RuleNetworkEvaluator.switchOnDoBetaNode(RuleNetworkEvaluator.java:571)
at org.drools.core.phreak.RuleNetworkEvaluator.evalBetaNode(RuleNetworkEvaluator.java:552)
at org.drools.core.phreak.RuleNetworkEvaluator.evalNode(RuleNetworkEvaluator.java:379)
at org.drools.core.phreak.RuleNetworkEvaluator.innerEval(RuleNetworkEvaluator.java:339)
at org.drools.core.phreak.RuleNetworkEvaluator.outerEval(RuleNetworkEvaluator.java:175)
at org.drools.core.phreak.RuleNetworkEvaluator.evaluateNetwork(RuleNetworkEvaluator.java:133)
at org.drools.core.phreak.RuleExecutor.reEvaluateNetwork(RuleExecutor.java:212)
at org.drools.core.phreak.RuleExecutor.evaluateNetworkAndFire(RuleExecutor.java:87)
at org.drools.core.concurrent.AbstractRuleEvaluator.internalEvaluateAndFire(AbstractRuleEvaluator.java:34)
at org.drools.core.concurrent.SequentialRuleEvaluator.evaluateAndFire(SequentialRuleEvaluator.java:43)
at org.drools.core.common.DefaultAgenda.fireLoop(DefaultAgenda.java:1067)
at org.drools.core.common.DefaultAgenda.internalFireAllRules(DefaultAgenda.java:1014)
at org.drools.core.common.DefaultAgenda.fireAllRules(DefaultAgenda.java:1006)
at org.drools.core.impl.StatefulKnowledgeSessionImpl.internalFireAllRules(StatefulKnowledgeSessionImpl.java:1337)
at org.drools.core.impl.StatefulKnowledgeSessionImpl.fireAllRules(StatefulKnowledgeSessionImpl.java:1328)
at org.drools.core.impl.StatefulKnowledgeSessionImpl.fireAllRules(StatefulKnowledgeSessionImpl.java:1312)
at com.sample.DroolsTest.main(DroolsTest.java:24)
We have used inferred facts extensively in other scenarios, but this is the first time we're using it with events. So we're not sure if this is capability (inferring facts based on correlated events) is supported.
If this is not supported, what's the best way to 'save' events that are required while allowing other events to be evicted / retracted from WM. We could use 'flags' to mark the events we want to keep, but we would like to avoid that approach if possible.
Thanks
As much I am a Drools fan why don't you just create a SQL table with start_datetime and end_datetime together with "before insert on" trigger that checks that the inserted value doesn't overlap any of the existing (confirmed) values in DB. If the trigger throws exception during the insert then you catch it and you know that an overlap exists.
I recommend adding $-sign in front of variable names then you can better distinct between fields and variables.
If you add a method boolean isOverlap(ReservationRequest) into the ReservationRequest class then you could write something like this:
when
$reservationRequest : ReservationRequest( )
not Reservation ( this.reservation.isOverlap($reservationRequest) )
then
insert( new Reservation ( $reservationRequest ) );
This should be sufficient. I also highly recommend to use variable name "reservationRequest" when you refer to ReservationRequest right now it is very confusing.
Related
I'm trying to work with lists in drools. I'm passing in a request which has a purchase list as part of it. I want to do several rules including checking if the size is correct, then if all elements are the same, if all purchases are authorized, ... I have the following code but I'm running into problems working with the list. Is this the right approach? Especially when checking for the size?
import com.rules.Purchase
import com.rules.PurchaseRequest
dialect "mvel"
global Boolean eligibleForRefund
rule "Check for list not equal to two elements" salience 10
when
PurchaseRequest(getPurchases != null, getPurchases.size() != 2)
then
drools.getKieRuntime().setGlobal("eligibleForRefund", false);
end
rule "Check for two purchases" salience 9
when:
$purchaseRequest: PurchaseRequest()
Number(intValue != 2) from accumulate(Purchase(getStatus() == "Approved") from $purchaseRequest.getPurchases(), count(1))
then
drools.getKieRuntime().setGlobal("eligibleForRefund", false);
end
rule "Check for the same purchases" salience 8
when:
$purchaseRequest: PurchaseRequest()
then
firstPurchase = $purchaseRequest.getPurchases().get(0).getCost();
hasAllElements = true;
for (Purchase purchase : $purchaseRequest.getPurchases()) {
if (purchase.getCost() != firstPurchase) {
hasAllElements = false;
}
}
drools.getKieRuntime().setGlobal("eligibleForRefund", hasAllElements);
end
Assuming that your class definition looks like this:
class PurchaseRequest {
private List<Purchase> purchases;
public List<Purchase> getPurchases() { return this.purchases; }
}
You should be pulling references out of the holder instead of constantly interacting with things via the getters. In other projects this helps with keep data consistent especially with shared resources. Recall that if you have a getter whose name matches the format getXyz, you can refer to it simply as xyz and drools will automagically map it to the getter function. This allows us to get the purchases via PurchaseRequest( $purchases: purchases ) since purchases will be mapped to getPurchases(). (Note that if purchases happened to be a public variable, it would have mapped to that first; but since it's private it falls back on the public getter that follows bean naming conventions.)
Second you use an accumulate in a very simple scenario where a collect would probably be more appropriate. Generally you'd use accumulate for more complicated "get things that look like this" sort of situations; but for simple matching, a collect works just as well.
The third rule needs the most work. You do not want to do this kind of business logic on the right hand side of your rule. There's a whole lot of ways you could go about checking that all the elements are the same -- if you've implemented equals/hashCode you could just shove everything into a set and confirm that the length of the set is still the length of the list; you could invert the rule to instead check for at least one item that's different; you could use accumulate or collect; ...
Finally --
Avoid saliences. They're bad design. Your rules should stand alone. You only need saliences here because your third rule sets both true and false. If instead you defaulted to true and then used the rules to override it to false, you could get away with having absolutely no saliences at all.
It's very unusual to use primitives for a global variable. I'm frankly not convinced that this will even work with a primitive. Globals work because the object is passed in by reference, and updated in the rules, and therefore the caller which retains the reference to the object will get the updated value. That doesn't work with primitives.
rule "Check for list not equal to two elements"
salience 1
when
PurchaseRequest($purchases: purchases != null)
List(size != 2) from $purchases
then
drools.getKieRuntime().setGlobal("eligibleForRefund", false);
end
rule "Check for two purchases"
salience 1
when:
PurchaseRequest( $purchases: purchases != null)
List( size != 2 ) from collect( Purchase(status == "Approved") from $purchases)
then
drools.getKieRuntime().setGlobal("eligibleForRefund", false);
end
// I've no idea what data type `getCost()` returns; I'm assuming "String"
rule "Check for the same purchases"
when:
PurchaseRequest($purchases: purchases != null)
// accumulate all of the costs into a set. if all costs are the same, set size = 1
$costs: Set() from accumulate( Purchase( $cost: cost ) from $purchases;
collectSet($cost))
then
drools.getKieRuntime().setGlobal("eligibleForRefund", $costs.size() == 1);
end
I want use the 'in' keyword in 'eval' function in which I am getting error that 'in' is not recognized by drools. So I have multiple values which I want check against a particular fact's attribute
when
$person : Person(PIN in ("123","456","789"))
then
//do something
end
//Like this I want use it in eval
when
$person : Person()
eval($person.PIN in ("123","456","789"))
then
//do something
end
But it is showing compile time error.
is there any other way to do it.
Edited
So I have some conditions in Decision Table where I want to use eval because other ways are not helpful in my scenario, below snapshot will Explain
SnapShot 1: Decision Table without eval()
SnapShot 2: Decision Table with eval()
Issue in first snapshot:
When compiling the spreadsheet the condition goes to the second lines object like below code : this is how it gets interpreted
when
personMap : PersonMap ()
basicEligiblePerson : Person( personalAddress.PIN in ($param) ) from
personMap.AddressesList
addresses : Address() from basicEligiblePerson.AddressesList
personalAddress : PersonalAddress() from addresses.PersonalAddress
then
basicEligiblePerson.setEligibility(true);
end
Issue in second snapshot :
When compiling this spreadsheet the condition goes to eval() function but 'in' keyword does not work in eval().
when
personMap : PersonMap ()
basicEligiblePerson : Person( personalAddress.PIN in ($param) ) from
personMap.AddressesList
addresses : Address() from basicEligiblePerson.AddressesList
personalAddress : PersonalAddress() from addresses.PersonalAddress
eval( personalAddress.PIN in ($param) )
then
basicEligiblePerson.setEligibility(true);
end
what should I do?
First sample given in your question is sufficient for the validation. You don't need to use eval.
when
$person : Person(PIN in ("123","456","789"))
then
//do something
end
If your requirement is to set eligibility to true for a given set of PINs, then you don't really need a decision table. I don't completely understand your POJO structure, so if Person class has a member variable addressList and AddressList class has a member personalAddress which has the member variable pin, you can achieve the results using the following rule. Please note that the nested fields are referred using the member variable names, not the class names. Also when you access the nested elements, if any of the elements can be null, please add the null check as well to avoid null pointer exceptions.
when
$basicEligiblePerson : Person( addressesList.personalAddress.pin in ("1234", "4567") )
then
$basicEligiblePerson.setEligibility(true);
end
This is the query I am trying to run in PostgreSQL:
SELECT * FROM message WHERE id IN (
SELECT unnest(message_ids) "mid"
FROM session_messages WHERE session_id = '?' ORDER BY "mid" ASC
);
However, I am not able do something:
create.selectFrom(Tables.MESSAGE).where(Tables.MESSAGE.ID.in(
create.select(DSL.unnest(..))
Because DSL.unnest is a Table<?>, which makes sense since it is trying to take a List-like object (mostly a literal) and convert it to table.
I have a feeling that I need to find a way to wrap the function around my field name, but I have no clue as to how to proceed.
NOTE. The field message_ids is of type bigint[].
EDIT
So, this is how I am doing it now, and it works exactly as expected, but I am not sure if this is the best way to do it:
Field<Long> unnestMessageIdField = DSL.field(
"unnest(" + SESSION_MESSAGES.MESSAGE_IDS.getName() + ")",
Long.class)
.as("mid");
Field<Long> messageIdField = DSL.field("mid", Long.class);
MESSAGE.ID.in(
ctx.select(messageIdField).from(
ctx.select(unnestMessageIdField)
.from(Tables.CHAT_SESSION_MESSAGES)
.where(Tables.CHAT_SESSION_MESSAGES.SESSION_ID.eq(sessionId))
)
.where(condition)
)
EDIT2
After going through the code on https://github.com/jOOQ/jOOQ/blob/master/jOOQ/src/main/java/org/jooq/impl/DSL.java I guess the right way to do this would be:
DSL.function("unnest", SQLDataTypes.BIGINT.getArrayType(), SESSION_MESSAGES.MESSAGE_IDS)
EDIT3
Since as always lukas is here for my jOOQ woes, I am going to capitalize on this :)
Trying to generalize this function, in a signature of sort
public <T> Field<T> unnest(Field<T[]> arrayField) {
return DSL.function("unnest", <??>, arrayField);
}
I don't know how I can fetch the datatype. There seems to be a way to get DataType<T[]> from DataType<T> using DataType::getArrayDataType(), but the reverse is not possible. There is this class I found ArrayDataType, but it seems to be package-private, so I cannot use it (and even if I could, it does not expose the field elementType).
Old PostgreSQL versions had this funky idea that it is OK to produce a table from within the SELECT clause, and expand it into the "outer" table, as if it were declared in the FROM clause. That is a very obscure PostgreSQL legacy, and this example is a good chance to get rid of it, and use LATERAL instead. Your query is equivalent to this one:
SELECT *
FROM message
WHERE id IN (
SELECT "mid"
FROM session_messages
CROSS JOIN LATERAL unnest(message_ids) AS t("mid")
WHERE session_id = '?'
);
This can be translated to jOOQ much more easily as:
DSL.using(configuration)
.select()
.from(MESSAGE)
.where(MESSAGE.ID).in(
select(field(name("mid"), MESSAGE.ID.getDataType()))
.from(SESSION_MESSAGES)
.crossJoin(lateral(unnest(SESSION_MESSAGES.MESSAGE_IDS)).as("t", "mid"))
.where(SESSION_MESSAGES.SESSION_ID.eq("'?'"))
)
The Edit3 in the question is quite close to a decent solution for this problem.
We can create a custom generic unnest method for jOOQ which accepts Field and use it in jOOQ query normally.
Helper method:
public static <T> Field<T> unnest(Field<T[]> field) {
var type = (Class<T>) field.getType().getComponentType();
return DSL.function("unnest", type, field);
}
Usage:
public void query(SessionId sessionId) {
var field = unnest(SESSION_MESSAGES.MESSAGE_IDS, UUID.class);
dsl.select().from(MESSAGE).where(
MESSAGE.ID.in(
dsl.select(field).from(SESSION_MESSAGES)
.where(SESSION_MESSAGES.SESSION_ID.eq(sessionId.id))
.orderBy(field)
)
);
}
I've had to create pairs of rules to retract my events. It seems they don't expire. I had wanted one-and-done events. You can see below, they use the default duration, zero.
So for example, if I exclude the retraction rules and then insert the RemoveConnectionEvent first and then insert the CreateConnectionEvent, the RemoveConnection rule will still fire. (Using an agenda listener in my unit tests)
My expectation of an event was that RemoveConnectionEvent would be ignored, it would not do anything if its conditions were not met immediately. I did not expect it to hang around and trigger the RemoveConnection rule once that rules conditions were met when the NewConnection rule responded to the CreateConnectionEvent.
To get my rules to behave as I expected, I created RetractedCreation, RetractedRemoval, and RetractedUpdate. This seems to be a hack. I am imagining a declared my events wrong.
Any ideas?
ps This was a pretty good Q&A but I am not using windows. It might infer that perhaps my hack is an 'explicit expiration policy'.
Test Event expiration in Drools Fusion CEPTest Event Expiration
Here is my rule.
package com.xxx
import com.xxx.ConnectedDevice
import com.xxx.RemoveConnectionEvent
import com.xxx.CreateConnectionEvent
import com.xxx.UpdateConnectionEvent
declare CreateConnectionEvent #role( event ) end
declare UpdateConnectionEvent #role( event ) end
declare RemoveConnectionEvent #role( event ) end
rule NewConnection
when
$connection : CreateConnectionEvent($newChannel : streamId)
not ConnectedDevice( streamId == $newChannel )
then
insert( new ConnectedDevice($newChannel) );
end
rule RetractedCreation
when
$creationEvent : CreateConnectionEvent($newChannel : streamId)
exists ConnectedDevice(streamId == $newChannel)
then
retract($creationEvent)
end
rule RemoveConnection
when
$remove : RemoveConnectionEvent($newChannel : streamId)
$connection : ConnectedDevice( streamId == $newChannel )
then
retract( $connection );
end
rule RetractedRemoval
when
$removalEvent : RemoveConnectionEvent($newChannel : streamId)
not ConnectedDevice(streamId == $newChannel)
then
retract($removalEvent)
end
rule UpdateConnection
when
$connectionUpdate : UpdateConnectionEvent($newChannel : streamId)
$connection : ConnectedDevice( streamId == $newChannel )
then
$connection.setLastMessage();
end
rule RetractedUpdate
when
$removalEvent : UpdateConnectionEvent($newChannel : streamId)
not ConnectedDevice(streamId == $newChannel)
then
retract($removalEvent)
end
This automatic expiry is a rather elusive feature. There's no concise definition when it'll work, and what needs to be done to make it work.
In your apparently simple case where you don't use temporal operators and expect that events are to be retracted after they have matched one rule I'd adopt the following strategy without wasting another thought on "inferred expiration" and "managed lifecycle".
Maybe you have a common (abstract) base class for your events; otherwise create a marker interface and attach it to all events. Let's call this type Event. Then, a single rule
rule "retract event"
salience -999999
when
$e: Event()
then
retract( $e );
end
will take care for all (Create, Update, Remove) events.
Edit You may also use the explicit setting for event expiry.
declare CreateConnectionEvent
#role( event )
#expires(0ms)
end
Make sure to use
KieBaseConfiguration config = ks.newKieBaseConfiguration();
config.setOption( EventProcessingOption.STREAM );
KieBase kieBase = kieContainer.newKieBase( config );
when creating the KieBase. I also recommend to "let the time pass", i.e., advance a pseudo clock or let the thread running a fireUntilHalt for a jiffy or two after fact insertion.
I'm having an object as below:
class License{
private field1;
private field2;
private boolean active;
private String activeMessage;
private boolean processed = false;
//Getter and setter methods
}
What I'm trying to do is, based on the values of field1, and field2, I need to set the isActive flag and a corresponding message. However, if either the rule for field1 or field2 is fired, I need to stop the rules processing. That is, I need to execute only 1 successful rule.
I read on a post that doing ksession.fireAllRules(1) will solve this. But the fireAllRules() method is not available in Drools 6. I also tried putting a return; statement at the end of each rule. That didn't help me either.
Finally, I ended up adding an additional field to my object called processed. So whenever I execute any rule, I set the processed flag to true. And if the flag is already set, then I do not execute any rule. This is my rules file:
rule "Check field1"
when
$obj : License(getField1() == "abc" && isProcessed() == false)
then
System.out.println("isProcessed >>>>>> "+$obj.isProcessed());
$obj.setActive(true);
$order.setActiveMessage("...");
$order.setProcessed(true);
end
rule "Check field2"
when
$obj : License(getField2() == "def" && isProcessed() == false)
then
System.out.println("isProcessed >>>>>> "+$obj.isProcessed());
$obj.setActive(true);
$order.setActiveMessage("...");
$order.setProcessed(true);
end
However, I see that even now both my rules are being fired. When I try to print the value of isProcessed(), it says true, even though I enter the rule only if isProcessed() is false.
This is how I'm calling the drools engine:
kieService = KieServices.Factory.get();
kContainer = kieService.getKieClasspathContainer();
kSession = kContainer.newStatelessKieSession();
kSession.execute(licenseObj);
It is not just 2 rules, I have a lot of rules, so controlling the rules execution by changing the order of the rules in the drl file is not an option. What is happening here? How can I solve this problem? I am sort of new to Drools, so I might be missing something here.
Thanks.
Your question contains a number of errors.
It is definitely not true that fireAllRules has disappeared in Drools 6. You might have looked at the javadoc index, to find four (4!) overloaded versions of this method in package org.kie.api.runtime.rule in the interface StatefulRuleSession.
You might easily avoid the problem of firing just one out of two rules by combining the triggering constraint:
rule "Check field1 and field2"
when
$lic: License(getField1() == "abc" || getField2() == "def" )
//...
then
$lic.setXxx(...);
end
You complain that both of your rules fire in spite of setting the processed flag in the fact. Here you are missing a fundamental point (which is covered in the Drools reference manual), i.e., the necessity of notifying the Engine whenever you change fact data. You should have used modify on the right hand side of your rules.
But even that would not have been good enough. Whenever an update is made due to some properties, a constraint should be added to avoid running the update over and over again. You might have written:
rule "Check field1 and field2"
when
$lic: License(getField1() == "abc" || getField2() == "def",
! active )
//...
then
modify( $lic ){ setActive( true ) }
end
You might even write this in two distinct rules, one for each field, and only one of these rules will fire...