We are working on a monitoring application in which we follow the processing of a task in a set of applications.
We have a set of drools rules matching our needs but we have some performance issues (we may have easily up to 50k objects in session).
We are looking for best pactices
This question is about bloolean flag usage.
We are working to remove most of org.drools.core.rule.constraint.MvelConstraint: Exception jitting: ... warns.
We have often such warn on boolean flags.
for example in:
rule "PropagateDeprecation"
when
$parent:BaseChainStep( $parent.Deprecated )
$child:BaseChainStep( $parent.Id == $child.Parent, !$child.Deprecated )
then
modify($child){
setDeprecated(true)
}
end
we have warn on both $parent.Deprecated and !$child.Deprecated.
We would like to understand why there is such warn on boolean flags.
We would like to know also the impacts of the warn on composed conditions.
For example in:
rule "App1_TriggerExpected"
when $chainStep:App1ChainStep(
HasChain
, HasParent
, !$chainStep.Deprecated
, Status in ("error", "closed")
, Places != null
, Analysis != null)
then
..
end
if we have the warn on the first condition HasChain, how is resolved the when clause ?
Does other conditions are evaluated too (with iteration on all App1ChainStep objects) or some "index" are still used to help ?
If its matter, we are using flags as boolean (and not Boolean) to ensure false value as default.
Edit:
The problem may be linked to extended classes. In our use case we have something like:
declare BaseChainStep
parent : GUID
deprecated : boolean
end
declare App1ChainStep extends BaseChainStep
// specific App1 fields
end
BaseChainStep fields may be manipulated in rules using App1ChainStep objects or BaseChainStep objects.
rule "deprecateApp1"
when $app1:App1ChainStep( BusinessLogicCondition )
then
modify($app1) {
setDeprecated(true)
}
end
Then the deprecated flag is propagated to App1 children using "PropagateDeprecation" rule.
Boolean flag causing warn are declared in BaseChainStep class.
Although you are deviating from the conventional way of accessing attributes, this should not trigger the warning you have reported. I can't reproduce this using 6.3.0. You should add (a) the Drools version (b) the Java code for a class BaseChainStep with which the problem can be reproduced with the rule as shown.
This is another (much simpler) way of writing rules combining boolean attributes:
rule bool1
when
X( big, fast )
then
System.out.println( "big fast X" );
end
You may even use boolean operators:
rule bool2
when
X( big && ! fast )
then
System.out.println( "big slow X" );
end
Note the simple use of field name, assuming conventional naming, e.g., big for the field, isBig and setBig for the accessors.
Related
rule "attaching AV and impact rating"
agenda-group "evaluate likelihood"
dialect "java"
when
Application($threatList:getThreatList())
$av:AttackVector()
exists $threat:Application.Threats(impact == "Disclose Information")from $threatList
exists AttackVector($av == AttackVector.REQUEST_MANIPULATION)
then
RiskRating riskRating=new RiskRating($threat.getImpactRating(),$av.getLikelihood(),$av.getName());
insertLogical(riskRating);
end
I am working on getting the object $threat in THEN part of the above-mentioned rule. If I run the above rule, it says:
Rule Compilation error : [Rule name='attaching AV and impact rating']
referee/security/attack/Rule_attaching_AV_and_impact_rating1426933818.java (7:1053) : $threat cannot be resolved
If I loop through it and get the value in the THEN part, it causes a CARTESIAN product and inserts the values a number of times in the session. My rule looks like this when I get the cartesian product.
rule "attaching AV and impact rating"
agenda-group "evaluate likelihood"
dialect "java"
when
Application($threatList:getThreatList())
$av:AttackVector()
exists $threat:Application.Threats(impact == "Disclose Information")from $threatList
$threat:Application.Threats(impact == "Disclose Information")from $threatList
exists AttackVector($av == AttackVector.REQUEST_MANIPULATION)
then
RiskRating riskRating=new RiskRating($threat.getImpactRating(),$av.getLikelihood(),$av.getName());
insertLogical(riskRating);
end
How do I get the value of $threat in THEN part without having the cartesian product?
Remove the exists operation entirely.
rule "attaching AV and impact rating"
agenda-group "evaluate likelihood"
dialect "java"
when
Application($threatList:getThreatList())
$av: AttackVector()
$threat: Application.Threats(impact == "Disclose Information")from $threatList
exists(AttackVector($av == AttackVector.REQUEST_MANIPULATION))
then
RiskRating riskRating=new RiskRating($threat.getImpactRating(),$av.getLikelihood(),$av.getName());
insertLogical(riskRating);
end
exists means "there is a thing in working memory that matches these conditions/looks like this". It's not used to actually extract or provide a reference to that matching instance. Simply remove the operator and it works as you need -- if there is an Application.Threats that matches your conditions, the rule triggers and the matching value is assigned to the $threat variable.
What you're running into is the fact that you have multiple threats that mean your condition, which is why you're having multiple triggers of the rule -- it will trigger once per matching Application.Threats. The exists keyword mitigates this because it only cares that at least one match exists, but you don't get a reference (because if there's four matches which one will be assigned to the variable? it doesn't make sense, logically.)
So you need to change your rule so that it won't fire multiple times and will instead only fire once when it finds a match. Usually you'd do this by making the consequences do something to working memory that makes the rule no longer eligible to be fired. In your example, you insert a RiskRating object; you could, then, check that no risk rating exists in your conditions:
not( RiskRating( /* insert criteria here or leave empty */ ) )
Alternatively you could retract something from working memory that your rule relies on to be present or a match. For example, if you don't need it for anything later on, you could retract the attack vector:
retract( $av )
Yet another option might be to try and update your getThreatList() implementation to return a Set instead so you don't have duplicates (assuming threats are considered duplicates based on the 'impact' field.) Or you could try to remove all Application.Threats instances that match the criteria from the threatlist being returned.
We simply don't know enough about your use case or rule set to know what data you need or what it looks like, but at the end of the day you simply need the rule to fire once and only once, so to do this you need to somehow update the rule to know that it's no longer valid.
I am new to Drools so please bear over with the terminology!
Can I make rule trigger even though an empty list is involved in the LHS?
I have the code below.
There are two rules. One rule that insert facts and another rule that work on facts.
Part of fact B is to hold a list of facts A.
If this list is not empty then I am able to work on fact B.
If this list is empty then I am not able to work on fact B.
How can I write the code so I am able to work on fact B even though the list is empty.
If I activate the line below "b.As.add(a);" then I can work on fact B.
If I deactivate the line below "b.As.add(a);" then I can work not on fact B.
declare A
nameA : String
end
declare B
nameB : String
As : java.util.ArrayList
end
rule "insertfacts"
when
then
A a = new A();
a.setNameA("A");
B b = new B();
b.setNameB("B");
b.As = new java.util.ArrayList();
b.As.add(a); // Only with this line rule checkfacts fires
insert( b );
end
rule "checkfacts"
when
$b : B();
$a : A() from $b.As;
then
// take action based on object $b
end
Thanks
Thomas S
To check if an object is present in working memory, you should use not() or exists() (depending on what you're trying to do.)
So your insert-facts rule should be like this:
rule "insertfacts"
when
not(B())
then
A a = new A();
a.setNameA("A");
B b = new B();
b.setNameB("B");
b.As = new java.util.ArrayList();
b.As.add(a); // Only with this line rule checkfacts fires
insert( b );
end
This way you won't end up with extra B instances because insertfacts will only trigger when B doesn't exist (so, once.)
To check for an empty list, you can do size == 0 as a check:
rule "As list is empty"
when
$b: B()
ArrayList( size == 0 ) from $b.As
then
// At this point you know that As is an empty list,
// so you can insert stuff if you want
end
Alternatively, if you want to check that a specific object is not in the list, you can use not memberOf.
rule "Some instance A is not in the As list"
when
$a: A() // the A instance we're trying to check
$b: B( $a not memberOf As )
then
// at this point As is a list of any size which doesn't contain the $a instance
end
Similarly there's memberOf for checking that the specific object is in the list:
rule "Some instance A is in the As list"
when
$a: A()
$b: B( $a memberOf As )
then
// As is a list of any size which DOES contain $a
end
Note that there are two complementary operators you could be using to check that something is or is not part of a list: memberOf and contains. They're roughly the same, just with the arguments in different orders. See this other question for more information about those two operators.
Of course, if you don't care at all about the contents of the As ArrayList, then just don't check anything against it in the "when" clause. If you're sure you'll never have a situation where it's undefined, you can even skip the null check.
rule "We don't care whether there's something in As"
when
$b: B( $as: As )
then
// at this point we have access to the variable $as which will be
// the arraylist; it may be empty, it may have stuff in it
end
... but you could always do a null check if you want to be safe:
$b: B( $as: As != null)
It was mentioned in a comment that you plan to write regular code in the "then" clause and loop through the list. Usually you don't want to do that -- if you just want to do work against the members of the list or a subset of the list (eg items of the list which meet certain criteria), Drools will implement the iteration out of the box for you.
As a simple example, let's say that you want to find all of the A instances inside of the list which have the name "PURCHASE" and update the name to be "VERIFIED_PURCHASE".
rule "Update PURCHASE to VERIFIED_PURCHASE"
when
B( $as: As != null )
$a: A( name == "PURCHASE" ) from $as
then
$a.name = "VERIFIED_PURCHASE"
end
(You could add an update call on the right hand side if it's important to reevaluate rule matches, but in this simple example it's not needed.)
What will happen here, and what's usually a little difficult for people new to Drools to wrap their heads around, is that Drools will internally iterate through the As array list and check each item to see if it matches the requested criteria (name == "PURCHASE"). For each item it finds that matches the criteria, it will trigger the right hand side.
So for this example, if As has 10 items and 3 are named "PURCHASE", this rule's consequences ("then" clause) will trigger 3 times, once per matched item.
If you need a collection of these matches on the right hand side, you can use collect or accumulate depending on how complex your use case is.
rule "Get all purchases"
when
B( $as: As != null )
$purchases: ArrayList() from collect( A( name == "PURCHASE" ) from $as )
then
// do something with $purchases here
end
Of course, you shouldn't then turn around and iterate through $purchases on the right hand side -- use the built-in way I showed previously.
The Drools engine is extremely good at optimizing performance; anything you put in the "when" clause gets optimized by Drools, which is why you want to do the iteration there (and let Drools leverage its own internal capabilities at that.)
The right hand side ("then" clause) is not optimized, so by putting the iteration and other logic on that side is going to make your rules perform worse in comparison. You may not notice the difference in a toy example like the ones we're working on in this question, but once you start processing hundreds or thousands of requests per minute, it will definitely start showing.
(I spent 10 years of my career supporting an embarrassingly huge collection of rules (scale is millions of rules) and I've never had the need to actually write a for-loop or any sort of iteration in the "then" clause. And the few places where I did find them as added by other engineers, I was able to remove them for non-negligible performance improvements. Let the framework do what it's good at.)
I would like the "then" clause to only execute one time, but it is executing for each child object in the list that matches.
If any item in the list meets the condition I want to break out and have only one execution of the then clause.
rule "Profile - Delinquent"
when
$c : CreditReportAll( $creditLiability : creditLiability )
$cs: CreditLiability( paymentPatternData.contains("X")) from $creditLiability
then
CreditUserSegment $cu = new CreditUserSegment();
$cu.setSegmentCode("delinquent");
$c.addUserSegmentToList($cu);
end
In order to only fire the rule once when any item in the $creditLiability list meets the condition, you either need to write your rule such that it doesn't iterate across the list, or you need to update your rule so that once it does fire, it changes the facts in working memory to not allow it to fire again.
No iteration
The easiest way to do this is to change your rule to not iterate across the list. To do this, we use the exists keyword like this:
rule "Profile - Delinquent"
when
$c : CreditReportAll( $creditLiability : creditLiability )
exists( CreditLiability( paymentPatternData.contains("X")) from $creditLiability )
then
CreditUserSegment $cu = new CreditUserSegment();
$cu.setSegmentCode("delinquent");
$c.addUserSegmentToList($cu);
end
The exists keyword will match when the there is at least one element present that matches the required condition. Note that we don't assign a variable to the result anymore, because it doesn't make any sense (eg. there's no assignment of $cs here; it would be ambiguous as to what it would even refer to.)
The downside to this approach is that if you update working memory in any other rule (eg by calling insert, modify, update, and so on), you may end up triggering this rule again because the conditions on the left hand side will still remain valid and matching. To alleviate this you may be able to leverage the no-loop rule attribute (depending on your setup). Otherwise you'll want to update your rule (or data in working memory) so that your rule is no longer valid.
Invalidate the rule
The other way to only trigger the rule once is to make the rule no longer valid to fire after it fires once. A trivial way to do this (likely not best practice in this case) would be to insert a flag into working memory and check on its presence. In this case I will use a simple string "DELINQUENT" as the flag.
rule "Profile - Delinquent"
when
not(String(this == "DELINQUENT"))
$c : CreditReportAll( $creditLiability : creditLiability )
$cs: CreditLiability( paymentPatternData.contains("X")) from $creditLiability
then
CreditUserSegment $cu = new CreditUserSegment();
$cu.setSegmentCode("delinquent");
$c.addUserSegmentToList($cu);
insert("DELINQUENT");
end
When the rule fires, it inserts a string that says "DELINQUENT" into working memory. The rule conditions are such that the rule only fires if this string doesn't exist in working memory. Thus after the first execution, the rule will not execute again unless a rule retracts that string.
This solution increases the memory footprint of the rule execution because there is more information in working memory. However unlike the other solution (which is more elegant), this version will not re-fire if another rule retriggers execution (eg. via update.)
I run a rule that contains a few patterns, I want to know which pattern failed:
I've tried to debug the code (drools 7.18.0), and didn't found the relevant place.
rule example:
rule "Trigger"
agenda-group "Trigger"
salience 100
when
$pcase : PCaseMgr()
D1($id: id, type != null, type == "AAA")
D2(aId == $id)
then
$pcase.printAnalyticsRuleLog(">>>>>>>>>>>>>>>>>>> In Trigger");
end
in the example above, if D1 pattern is passed, and D2 pattern is failed, where in the code (of drools 7.18.0) can i see if the pattern was failed?
You can't. Because of the algorithm Drools uses internally, patterns are decomposed into nodes and nodes can be shared among multiple rules in your knowledge base. If you really need to know why a rule was not fire, then you can create other rules that will tell you that. In your example, you could create something like this:
rule "No Trigger because of No D2"
agenda-group "Trigger"
salience 100
when
$pcase : PCaseMgr()
D1($id: id, type != null, type == "AAA")
not D2(aId == $id)
then
$pcase.printAnalyticsRuleLog(">>>>>>>>>>>>>>>>>>> No Trigger because no D2");
end
Hope it helps,
I'm trying to write a rule to calculate prices for an insurance product based on conditions. In the 'when' I'm using an object called AdditionalDriver, which contains the details for drivers other than the policy holder. From this, different prices can be calculated based on whether the additional driver is a parent, friend, spouse etc. See below:
when
AdditionalDriver($relToProp : relationToProposer)
then
String relToProp = $relToProp;
if(!relToProp.equals("P"))
{
//prices
}
end
"P" = parent.
This rule works when an additional driver has been added. However, if there is no additional driver, then the object is empty, and so the rule does not run. What do I need to do to get this rule to run, even when the object is empty?
Thanks in advance.
You should write one rule for each of the relative or acquaintance classes:
when
PolicyHolder( $phid: id )
AdditionalDriver( relationToProposer == "P", belongsTo == $phid )
then
//prices
end
For no additional driver being requested, write a rule
when
PolicyHolder( $phid: id )
not AdditionalDriver( belongsTo == $phid )
then
// cheaper prices
end
Don't use conditional statement in your consequences to further distinguish facts. This is a code smell.