Unnecessary rule consequence triggering from accumulate - drools

Note: A better question is written about this topic. Please vote for this one to be closed.
Link to new question
I ran into a weird case which i'm trying to understand why it happens. The issue is where a rule is constantly triggering it's consequence for it's last fact, without anything related to it being changed. I should note that i am using drools 7.0.0
Behavior analysis
I have the following rules:
rule "TicketsBoughtPerClass"
when
$ticketClass : TicketClass($customer : customer)
accumulate(
Ticket
(
customer != null,
customer == $customer,
ticketPrice >= $ticketClass.startPriceRange,
ticketPrice <= $ticketClass.endPriceRange
);
$ticketCount : sum(1)
)
then
System.out.println("Total " + $ticketCount + " bought tickets for " + $ticketClass.getClassName());
insertLogical(new TotalTicketsBoughtForClass($ticketClass, $ticketCount));
end
rule "TicketsNeededForBonus"
when
$ticketClass : TicketClass($minTicketsNeededForBonus : minTicketsNeededForBonus)
TotalTicketsBoughtForClass(ticketClass == $ticketClass, ticketCount < $minTicketsNeededForBonus, $ticketCount : ticketCount)
then
//Do something based on ($minTicketsNeededForBonus - $ticketCount)
end
The idea is to count the number of Ticket objects that are within a price range of a TicketClass for a Customer. However, as i mentioned, regardless of the price of the Ticket, there is always a rule trigger for the last inserted fact.
I added <- no match for the triggers of interest.
Here is a sample output:
Ticket classes (inserting ticketClass facts):
First class - Start price range: 200, End price range: 300
Second class - Start price range: 100, End price range: 199
Third class - Start price range: 50, End price range: 99
Buying tickets:
Bought ticket #0 for 60$ (insert)
Bought ticket #1 for 199$ (insert)
Bought ticket #2 for 250$ (insert)
Calling initial fireAllRules()
Total 1 bought tickets for Third class
Total 1 bought tickets for Second class
Total 1 bought tickets for First class
Changed ticket #0 from 60$ to 168$ (update)
fireAllRules() called
Total 0 bought tickets for Third class
Total 2 bought tickets for Second class
Changed ticket #0 from 168$ to 233$ (update)
fireAllRules() called
Total 0 bought tickets for Third class <- no match
Total 1 bought tickets for Second class
Total 2 bought tickets for First class
Changed ticket #0 from 233$ to 230$ (update)
fireAllRules() called
Total 0 bought tickets for Third class <- no match
Total 2 bought tickets for First class
Changed ticket #0 from 230$ to 283$ (update)
fireAllRules() called
Total 0 bought tickets for Third class <- no match
Total 2 bought tickets for First class
Changed ticket #0 from 283$ to 167$ (update)
fireAllRules() called
Total 0 bought tickets for Third class <- no match
Total 2 bought tickets for Second class
Total 1 bought tickets for First class
Changed ticket #0 from 167$ to 24$ (update)
fireAllRules() called
Total 0 bought tickets for Third class <- no match
Total 1 bought tickets for Second class
Changed ticket #0 from 24$ to 1$ (update)
fireAllRules() called
Total 0 bought tickets for Third class <- no match
Changed ticket #0 from 1$ to 0$ (update)
fireAllRules() called
Total 0 bought tickets for Third class <- no match
Changed ticket #0 from 0$ to 8$ (update)
fireAllRules() called
Total 0 bought tickets for Third class <- no match
Changed ticket #0 from 8$ to 40$ (update)
fireAllRules() called
Total 0 bought tickets for Third class <- no match
Unless there is something i have misunderstood, this should not be the case.
Debugging
I decided to dig in a little deeper into the drools core to get some answers myself. The search led me to the evaluation of the updated tuples in the accumulate node in PhreakAccumulateNode.doRightUpdates, which is where this additional trigger seems to be coming from. Just before the method doRightUpdatesProcessChildren, the following code is present:
// if LeftTupleMemory is empty, there are no matches to modify
if ( leftTuple != null ) {
if ( leftTuple.getStagedType() == LeftTuple.NONE ) {
trgLeftTuples.addUpdate( leftTuple ); //<----
}
doRightUpdatesProcessChildren( ARGS );
}
In short, this code always adds the first left tuple that is in AccmulateMemory as being updated, even though it isn't, which is what causes the RuleTerminalNode to always fire it's consequence for this tuple.
My current dilemma is that I don't understand if this call for trgLeftTuples.addUpdate( leftTuple ) is done on purpose, because when I took a look at the code in PhreakJoinNode, extremely similar code is present when processing the updated tuples, but this call is missing before doRightUpdatesProcessChildren is called.
The main question i have about this if it is the expected behavior, and if so, why?.

If I got you right, and you want to eliminate rule triggering when there were found no single ticket for your rule, then you should add additional constraint.
rule "TicketsBoughtPerClass"
when
$ticketClass : TicketClass($customer : customer)
accumulate(
Ticket
(
customer != null,
customer == $customer,
ticketPrice >= $ticketClass.startPriceRange,
ticketPrice <= $ticketClass.endPriceRange
);
$ticketCount : sum(1);
$ticketCount > 0
)
then
System.out.println("Total " + $ticketCount + " bought tickets for " + $ticketClass.getClass().getName());
end
Without $ticketCount > 0 constraint, you have the only constraint for rule to be triggered TicketClass()

Related

Kafka aggregation: Issue when grouping key changes

We are using kafka KTable for aggregation and below is the kind of data we receive in input
Input data - Transaction Detail (transaction Id, status, category, amount,.. )
We group the above based on below
Grouping Key - (status, category)
App logic
Grouped Stream. Aggregate(() -> new Instance(), (key, newVal, aggVal) - > addAmount(newVal). (key, oldVal, aggVal) - > removeAmount(oldVal));
Let's say we get below stream of data as (transaction I'd, status, category, amount)
1 - 1, pending, cash, 10 // (pending, cash) - 10 aggregated value
2 - 2, pending, cash, 20 // (pending, cash) - 30
3 - 3, actual, card, 15 //(actual, card) - 15
4 - 1. Pending, card, 9 //(pending, cash) - 30, (pending, card) - 9 - - - This is where we are getting problem
In #4 though the update the on the same transactionId 1, but the grouping key changes (from cash to card), now since the grouping has changed it doesnt call removeAmoutn() method but only addAmount() method is called.
Any idea on how we can be solve this issue where if the grouping changes it should take care of earlier aggregated data as well.
I found similar use case here
https://stackoverflow.com/a/42685866/2699756
But not sure what was done yo fix this.

Indicate more than one record with matching fields

How can I indicate multiple records with the same Invoice number, but a different Sales Person ID? Our commissions can be split into multiple Salespeople, so there can be two different Salespeople per an invoice.
For example:
Grouped by: Sales Person ID (No Changing this option)
These records are in the Group Footer.
Sales Person ID: Invoice: Invoice Amt: Commissions: (Indicator)
4433 R100 20,000 3,025 * More than one record on the same invoice with a different sales person
4450 R096 1,987 320
4599 R100 20,000 3,025 * More than one record on the same invoice with a different sales person
4615 R148 560 75
4777 R122 2,574 356
If your report has less than 1000 invoices, you may try something like this.
This will return true when a second ocurrence of the invoice shows up. Then you can make something like set the row background do red.
Global NumberVar Array invoices;
numbervar nextIndex := count(invoices) + 1;
if nextIndex <= 1000 and not ({Result.InvoiceNumber} in invoices) then (
redim invoices [nextIndex];
invoices[nextIndex] := {Result.InvoiceNumber};
true;
)
else false;
If you want to detect the first occurrence, you will need something more sophisticated.
I think a SQL Expression Field would be a good way to achieve the result you want. You already have an InvoiceNo in each row of data. You just need a SQL Expression Field that uses that INvoiceNo to execute a query to count the number of salespersons who get a commission.
Something along the lines of:
(
Select Count(Sales_Person_Id)
From [Table]
Where [Table].InvoiceNo = InvoiceNo
)
This will return an integer value that represents the number of salespersons who are associated with one invoice. You can either drop the SQL Expression Field in your Indicator column, or write some other formula to do something special.

Filter portal for most recently created record by group

I have a portal on my "Clients" table. The related table contains the results of surveys that are updated over time. For each combination of client and category (a field in the related table), I only want the portal to display the most recently collected row.
Here is a link to a trivial example that illustrates the issue I'm trying to address. I have two tables in this example (Related on ClientID):
Clients
Table 1 Get Summary Method
The Table 1 Get Summary Method table looks like this:
Where:
MaxDate is a summary field = Maximum of Date
MaxDateGroup is a calculated field = GetSummary ( MaxDate ;
ClientIDCategory )
ShowInPortal = If ( Date = MaxDateGroup ; 1 ; 0 )
The table is sorted on ClientIDCategory
Issue 1 that I'm stumped on: .
ShowInPortal should equal 1 in row 3 (PKTable01 = 5), row 4 (PKTable01 = 6), and row 6 (PKTable01 = 4) in the table above. I'm not sure why FM is interpreting 1Red and 1Blue as the same category, or perhaps I'm just misunderstanding what the GetSummary function does.
The Clients table looks like this:
Where:
The portal records are sorted on ClientIDCategory
Issue 2 that I'm stumped on:
I only want rows with a ShowInPortal value equal to 1 should appear in the portal. I tried creating a portal filter with the following formula: Table 1 Get Summary Method::ShowInPortal = 1. However, using that filter removes all row from the portal.
Any help is greatly appreciated.
One solution is to use ExecuteSQL to grab the Max Date. This removes the need for Summary functions and sorts, and works as expected. Propose to return it as number to avoid any issues with date formats.
GetAsTimestamp (
ExecuteSQL (
"SELECT DISTINCT COALESCE(MaxDate,'')
FROM Survey
WHERE ClientIDCategory = ? "
; "" ; "";ClientIDCategory )
)
Also, you need to change the ShowInPortal field to an unstored calc field with:
If ( GetAsNumber(Date) = MaxDateGroupSQL ; 1 ; 0 )
Then filter the portal on this field.
I can send you the sample file if you want.

Drools rule for counting number of times an service was performed?

I am new to Drools and am trying to design some rules for a dental insurance application. The system will basically let users known when a procedure they are about to perform may not be covered by insurance based on the history of previously performed services. It will have other rules as well, which may be age based, but I can handle those.
My facts are:
Patient - Listing of patient information.
Services - Previously performed services. (ie: serviceCode=D1234, datePerformed=Date)
Alerts - (alertName = "Xrays 2/12 Month Period")
I need a rule which says WHEN a patient has had D1234 performed 2 or more times in the last 12 month period THEN add a warning saying that D1234 may not be covered by insurance until 12 months after date of last D1234 service.
Further complicating the situation is the fact that there could be groups of codes which are limited in the same way. So, the codes listed in the rule may be an array of codes and not just a single one, but the rule would still need to fire.
I could write a service to fetch all the services performed and just do it like that, but I would think it is nicer to just throw all the facts (previous services, etc) into Drools and let it work it all out. This way I could have a rule process run for each patient with their alerts and previous services as facts and the result with be a list of warnings.
Can someone help me understand how to write a rule like I need above?
I will show you some examples of the different things you need to do, and leave it to you to combine them into a rule which works within your application.
Warning - I have not executed the following examples, so there may be bugs/typos.
First, the following piece of code will collect up all services performed on each patient in the working memory.
rule "Count patient services"
when
$patient : Patient()
$serviceList : ArrayList() from collect (
Service(patientId == $patient.id)
)
then
System.out.println("Patient " + $patient.id
+ " has received " + $serviceList.size() + " services.");
end
The following matches when a patient has received more than 2 services with a particular code:
$serviceList : ArrayList( size > 2 ) from collect (
Service(
patientId == $patient.id,
serviceCode == "D1234"
)
)
The following matches when a patient has received more than 2 services matching a list of codes.
$serviceList : ArrayList( size > 2 ) from collect (
Service(
patientId == $patient.id,
serviceCode in ("D1234", "E5678")
)
)
The following finds the most recent matching service date:
accumulate (
Service(
patientId == $patient.id,
serviceCode in ("D1234", "E5678"),
$datePerformed: datePerformed
);
$mostRecentDate: max($datePerformed)
)
Likewise, you can add constraints on dates or other attributes.
An effective mechanism for maintaining code groups would be to insert group membership facts. A spreadsheet or web decision table can do this easily, or you could query a database and insert them via the API. i.e.
insert( new ServiceGroup( "HighCostService", "D1234" ) );
insert( new ServiceGroup( "HighCostService", "D5678" ) );
Then you can do the matching with constraints like this:
$highCostServices : ArrayList() from accumulate (
ServiceGroup( group == "HighCostService", $serviceCode ),
init( ArrayList list = new ArrayList(); ),
action( list.add($serviceCode); ),
reverse( list.remove($serviceCode); ),
result(list)
)
$serviceList : ArrayList( size > 2 ) from collect (
Service(
patientId == $patient.id,
serviceCode in $highCostServices
)
)
n.b. - Accumulators are rather easy to get wrong, so I usually put a few unit tests around them. The above code was written freehand in here without running it anywhere, so you may well be lucky if it works without corrections.
For more details, see the manual:
http://docs.jboss.org/drools/release/5.5.0.Final/drools-expert-docs/html_single/
Matching items which are in a list:
4.8.3.3.10.10. The operators in and not in (compound value restriction)
Collecting lists of matched facts:
4.8.3.7.3. Conditional Element collect
Accumulators for calculating max, min, sum, etc values for collections:
4.8.3.7.4.1. Accumulate CE (preferred syntax)

TSQL Syntax, Replace Existing "Wrong Value" with previous "Correct Value"

I have an application that makes an entry every hour in a MS SQL database.
The last entry on the 12th FEB is a zero value and is showing in my weekly report.
What I want to do is take the value from the previous count and enter into the filed instead of the zero value.
Can someone offer some advice on how to this because it is beyond my TSQL skills?
SELECT * FROM [dbo].[CountDetails]
WHERE [updateTime] < '2013-02.13'
AND [updateTime] > '2013-02.12'
AND ( DATEPART(hh,[updateTime])= '22' OR DATEPART(hh,[updateTime])= '23' )
Note: The application is supposed to zero the count a Midnight but on the 12th FEB it happened early and I know why.
EDIT: There are 5 IP addresses in total and 6 counters in total because 192.168.168.11 has 2 counters. So 2111 to 2116 is an entire entry for all available counters at 22:58 and 2117 to 2122 is an entire entry for all available counters at 23:58. I need to replace the 23:58 values with the corresponding value from 22:58.
Guessing here, but an update that joins on the ipAddress, counterNumber, and the datetime excluding fractional seconds, separated by an hour (do the SELECT part first for safety):
UPDATE b
SET count = a.count
-- SELECT *
FROM dbo.CountDetails a
JOIN dbo.CountDetails b ON a.ipAddress = b.ipAddress AND a.counterNumber = b.counterNumber
AND CONVERT(VARCHAR(20),b.updateTime,120) = CONVERT(VARCHAR(20),DATEADD(HOUR,1,a.updateTime),120)