diff --git a/drools-ansible-rulebook-integration-api/src/main/java/org/drools/ansible/rulebook/integration/api/rulesengine/RegisterOnlyAgendaFilter.java b/drools-ansible-rulebook-integration-api/src/main/java/org/drools/ansible/rulebook/integration/api/rulesengine/RegisterOnlyAgendaFilter.java index 07a4a1a1..68afa419 100644 --- a/drools-ansible-rulebook-integration-api/src/main/java/org/drools/ansible/rulebook/integration/api/rulesengine/RegisterOnlyAgendaFilter.java +++ b/drools-ansible-rulebook-integration-api/src/main/java/org/drools/ansible/rulebook/integration/api/rulesengine/RegisterOnlyAgendaFilter.java @@ -2,6 +2,10 @@ import org.drools.ansible.rulebook.integration.api.domain.RuleMatch; import org.drools.core.common.InternalFactHandle; +import org.drools.core.reteoo.LeftTuple; +import org.drools.core.reteoo.RightTuple; +import org.drools.core.reteoo.RuleTerminalNodeLeftTuple; +import org.drools.core.reteoo.Tuple; import org.kie.api.runtime.rule.AgendaFilter; import org.kie.api.runtime.rule.FactHandle; import org.kie.api.runtime.rule.Match; @@ -56,7 +60,7 @@ public boolean accept(Match match) { } for (InternalFactHandle fh : fhs) { - if (fh.isEvent()) { + if (fh.isEvent() && discardMatchedEvent(fh)) { eventsToBeDeleted.add(fh); } } @@ -64,6 +68,36 @@ public boolean accept(Match match) { return validMatch; } + private boolean discardMatchedEvent(InternalFactHandle fh) { + // if an event can match multiple rules and is still part of a partial match it shouldn't be discarded yet + return !(rulesExecutorSession.isMatchMultipleRules() && hasPartialMatch(fh)); + } + + private static boolean hasPartialMatch(InternalFactHandle fh) { + // check if all the tuples created from this fact reached the terminal node, otherwise it is a partial match + for (RightTuple rt = fh.getLinkedTuples().getFirstRightTuple(0); rt != null; rt = rt.getHandleNext()) { + if (isPartialMatch(rt)) { + return true; + } + } + for (LeftTuple lt = fh.getLinkedTuples().getFirstLeftTuple(0); lt != null; lt = lt.getHandleNext()) { + if (isPartialMatch(lt)) { + return true; + } + } + return false; + } + + private static boolean isPartialMatch(Tuple tuple) { + // the tuple is a partial match if never reached a terminal node + for (; tuple != null; tuple = tuple.getFirstChild()) { + if (tuple instanceof RuleTerminalNodeLeftTuple) { + return false; + } + } + return true; + } + public List finalizeAndGetResults(boolean event) { rulesExecutorSession.registerMatchedEvents(eventsToBeDeleted); for (FactHandle toBeDeleted : eventsToBeDeleted) { diff --git a/drools-ansible-rulebook-integration-api/src/test/java/org/drools/ansible/rulebook/integration/api/MultipleRuleMatchTest.java b/drools-ansible-rulebook-integration-api/src/test/java/org/drools/ansible/rulebook/integration/api/MultipleRuleMatchTest.java index 314fe3b0..e83c3368 100644 --- a/drools-ansible-rulebook-integration-api/src/test/java/org/drools/ansible/rulebook/integration/api/MultipleRuleMatchTest.java +++ b/drools-ansible-rulebook-integration-api/src/test/java/org/drools/ansible/rulebook/integration/api/MultipleRuleMatchTest.java @@ -16,11 +16,11 @@ package org.drools.ansible.rulebook.integration.api; -import java.util.List; - import org.junit.Test; import org.kie.api.runtime.rule.Match; +import java.util.List; + import static org.assertj.core.api.Assertions.assertThat; public class MultipleRuleMatchTest { @@ -128,4 +128,124 @@ public void processEventsWithMatchMultipleRules_shouldMatchMultipleRules() { rulesExecutor.dispose(); } + + @Test + public void retainLeftPartialMatchesWithMatchMultipleRules() { + checkPartialMatchesWithMatchMultipleRules(true, true); + } + + @Test + public void discardLeftPartialMatchesWithMatchMultipleRules() { + checkPartialMatchesWithMatchMultipleRules(false, true); + } + + @Test + public void retainRightPartialMatchesWithMatchMultipleRules() { + checkPartialMatchesWithMatchMultipleRules(true, false); + } + + @Test + public void discardRightPartialMatchesWithMatchMultipleRules() { + checkPartialMatchesWithMatchMultipleRules(false, false); + } + + private static void checkPartialMatchesWithMatchMultipleRules(boolean matchMultipleRules, boolean partialOnLeft) { + String rules = + "{\n" + + " \"match_multiple_rules\":" + matchMultipleRules + ",\n" + + " \"rules\": [\n" + + " {\n" + + " \"Rule\": {\n" + + " \"name\": \"R1\",\n" + + " \"condition\": {\n" + + " \"AllCondition\": [\n" + + " {\n" + + " \"EqualsExpression\": {\n" + + " \"lhs\": {\n" + + " \"Event\": \"i\"\n" + + " },\n" + + " \"rhs\": {\n" + + " \"Integer\": 0\n" + + " }\n" + + " }\n" + + " }\n" + + " ]\n" + + " },\n" + + " \"actions\": [\n" + + " {\n" + + " \"Action\": {\n" + + " \"action\": \"debug\",\n" + + " \"action_args\": {\n" + + " \"msg\": \"First one matches\"\n" + + " }\n" + + " }\n" + + " }\n" + + " ],\n" + + " \"enabled\": true\n" + + " }\n" + + " },\n" + + " {\n" + + " \"Rule\": {\n" + + " \"name\": \"R2\",\n" + + " \"condition\": {\n" + + " \"AllCondition\": [\n" + + " {\n" + + " \"EqualsExpression\": {\n" + + " \"lhs\": {\n" + + " \"Event\": \" " + (partialOnLeft ? "i" : "j") + " \"\n" + + " },\n" + + " \"rhs\": {\n" + + " \"Integer\": 0\n" + + " }\n" + + " }\n" + + " },\n" + + " {\n" + + " \"EqualsExpression\": {\n" + + " \"lhs\": {\n" + + " \"Event\": \"" + (partialOnLeft ? "j" : "i") + " \"\n" + + " },\n" + + " \"rhs\": {\n" + + " \"Integer\": 0\n" + + " }\n" + + " }\n" + + " }\n" + + " ]\n" + + " },\n" + + " \"actions\": [\n" + + " {\n" + + " \"Action\": {\n" + + " \"action\": \"debug\",\n" + + " \"action_args\": {\n" + + " \"msg\": \"Second one matches\"\n" + + " }\n" + + " }\n" + + " }\n" + + " ],\n" + + " \"enabled\": true\n" + + " }\n" + + " }\n" + + " ]\n" + + "}"; + + RulesExecutor rulesExecutor = RulesExecutorFactory.createFromJson(rules); + + List matchedRules = rulesExecutor.processEvents("{ \"i\" : 0 }").join(); + assertThat(matchedRules).hasSize(1); + assertThat(matchedRules.stream().map(m -> m.getRule().getName())).contains("R1"); + + matchedRules = rulesExecutor.processEvents("{ \"j\" : 0 }").join(); + assertThat(matchedRules).hasSize(matchMultipleRules ? 1 : 0); + + if (matchMultipleRules) { + // when multiple match is allowed i=0 should be retained and now used to also fire R2 + assertThat(matchedRules.stream().map(m -> m.getRule().getName())).contains("R2"); + // if both rules fired now the working memory should be empty + assertThat(rulesExecutor.getAllFacts()).isEmpty(); + } else { + // if R2 never fired j=0 should still be there + assertThat(rulesExecutor.getAllFacts().size()).isEqualTo(1); + } + + rulesExecutor.dispose(); + } }