This is an automated email from the ASF dual-hosted git repository.
tkobayas pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/incubator-kie-drools.git
The following commit(s) were added to refs/heads/main by this push:
new fa5d1b8dd4 [incubator-kie-drools-6410] Multiple rules in an activation
group can… (#6419)
fa5d1b8dd4 is described below
commit fa5d1b8dd4d0cefddf228733c0a16d8a2ace2b43
Author: Toshiya Kobayashi <[email protected]>
AuthorDate: Fri Sep 5 09:16:18 2025 +0900
[incubator-kie-drools-6410] Multiple rules in an activation group can…
(#6419)
* [incubator-kie-drools-6410] Multiple rules in an activation group can
fire for matches with multiple conditions
* add comment
* cleanup
---
.../drools/core/phreak/PhreakRuleTerminalNode.java | 16 +-
.../ActivationGroupDualFactMatchTest.java | 192 +++++++++++++++++++++
2 files changed, 198 insertions(+), 10 deletions(-)
diff --git
a/drools-core/src/main/java/org/drools/core/phreak/PhreakRuleTerminalNode.java
b/drools-core/src/main/java/org/drools/core/phreak/PhreakRuleTerminalNode.java
index 53d84e4cf6..62d69beb16 100644
---
a/drools-core/src/main/java/org/drools/core/phreak/PhreakRuleTerminalNode.java
+++
b/drools-core/src/main/java/org/drools/core/phreak/PhreakRuleTerminalNode.java
@@ -101,14 +101,10 @@ public class PhreakRuleTerminalNode {
return;
}
- PropagationContext pctx;
- if ( rtnNode.getRule().isNoLoop() ) {
- pctx = leftTuple.findMostRecentPropagationContext();
- if ( sameRules(rtnNode, pctx.getTerminalNodeOrigin()) ) {
- return;
- }
- } else {
- pctx = leftTuple.getPropagationContext();
+ // most recent PropagationContext is required to maintain the right
recency which triggers the match
+ PropagationContext pctx = leftTuple.findMostRecentPropagationContext();
+ if ( rtnNode.getRule().isNoLoop() && sameRules(rtnNode,
pctx.getTerminalNodeOrigin()) ) {
+ return;
}
int salienceInt = getSalienceValue(rtnNode, ruleAgendaItem, leftTuple,
reteEvaluator);
@@ -117,8 +113,8 @@ public class PhreakRuleTerminalNode {
activationsManager.getAgendaEventSupport().fireActivationCreated(leftTuple,
activationsManager.getReteEvaluator());
- if ( rtnNode.getRule().isLockOnActive() && pctx.getType() !=
PropagationContext.Type.RULE_ADDITION ) {
- pctx = leftTuple.findMostRecentPropagationContext();
+ if ( rtnNode.getRule().isLockOnActive() &&
+ leftTuple.getPropagationContext().getType() !=
PropagationContext.Type.RULE_ADDITION ) {
InternalAgendaGroup agendaGroup =
executor.getRuleAgendaItem().getAgendaGroup();
if (blockedByLockOnActive(rtnNode.getRule(), pctx, agendaGroup)) {
activationsManager.getAgendaEventSupport().fireActivationCancelled(leftTuple,
reteEvaluator, MatchCancelledCause.FILTER );
diff --git
a/drools-model/drools-model-codegen/src/test/java/org/drools/model/codegen/execmodel/ActivationGroupDualFactMatchTest.java
b/drools-model/drools-model-codegen/src/test/java/org/drools/model/codegen/execmodel/ActivationGroupDualFactMatchTest.java
new file mode 100644
index 0000000000..f14423e74e
--- /dev/null
+++
b/drools-model/drools-model-codegen/src/test/java/org/drools/model/codegen/execmodel/ActivationGroupDualFactMatchTest.java
@@ -0,0 +1,192 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.drools.model.codegen.execmodel;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.kie.api.event.rule.AfterMatchFiredEvent;
+import org.kie.api.event.rule.DefaultAgendaEventListener;
+import org.kie.api.runtime.KieSession;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class ActivationGroupDualFactMatchTest extends BaseModelTest {
+
+ public static class FactWrapper {
+
+ private final String id;
+ private boolean flag;
+
+ public FactWrapper(String id) {
+ this.id = id;
+ this.flag = false;
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public boolean isFlag() {
+ return flag;
+ }
+
+ public void setFlag(boolean flag) {
+ this.flag = flag;
+ }
+
+ @Override
+ public String toString() {
+ return "FactWrapper{id='" + id + "', flag=" + flag + '}';
+ }
+ }
+
+ /**
+ * Drools Rule Language (DRL) definition containing three interconnected
rules
+ * that demonstrate activation group behavior with agenda group
transitions.
+ * <p>
+ * Rule Definitions:
+ * <p>
+ * "Setup Rule - GroupA":
+ * - Triggers on dynamic facts with flag=false
+ * - Modifies the fact to set flag=true
+ * - Switches agenda focus to GroupB
+ * - Not in any activation group (can fire multiple times)
+ * <p>
+ * "Rule A - GroupA":
+ * - Requires both static fact (flag=true) AND dynamic fact (flag=true)
+ * - In activation-group "TestGroup"
+ * - Should NEVER fire due to activation group mutual exclusion
+ * - Also in GroupA agenda group
+ * <p>
+ * "Rule B - GroupB":
+ * - Requires only dynamic fact (flag=true)
+ * - In activation-group "TestGroup" (same as Rule A)
+ * - Fires first, preventing Rule A from executing
+ * - Executes in GroupB agenda group
+ * <p>
+ * Execution Flow:
+ * 1. Setup Rule fires, modifies dynamic fact, switches to GroupB
+ * 2. Rule B fires in GroupB (satisfies activation group)
+ * 3. Rule A cannot fire because activation group is already satisfied
+ */
+ private static final String DRL = """
+ package com.example
+
+ import %s.FactWrapper;
+
+ global org.kie.api.runtime.KieSession ksession;
+
+ rule "Setup Rule - GroupA"
+ agenda-group "GroupA"
+ when
+ $d: FactWrapper(id matches "dynamic-\\\\d+", flag == false)
+ then
+ modify($d) { setFlag(true) };
+ ksession.getAgenda().getAgendaGroup("GroupB").setFocus();
+ end
+
+ // This rule should never fire, but fires twice. Switching the
order of the conditions
+ // (putting StaticFact first) will make the test pass.
+ rule "Rule A - GroupA"
+ agenda-group "GroupA"
+ activation-group "TestGroup"
+ when
+ DynamicFact: FactWrapper(id matches "dynamic-.*", flag ==
true)
+ StaticFact: FactWrapper(id == "static", flag == true)
+ then
+ end
+
+ rule "Rule B - GroupB"
+ agenda-group "GroupB"
+ activation-group "TestGroup"
+ when
+ DynamicFact: FactWrapper(id matches "dynamic-.*", flag ==
true)
+ then
+ end
+ """.formatted(ActivationGroupDualFactMatchTest.class.getName());
+
+ @ParameterizedTest
+ @MethodSource("parameters")
+ void testRuleAMatchingTwoFactsButNeverFires(RUN_TYPE runType) {
+ // === PHASE 1: Rule Compilation and Knowledge Base Setup ===
+
+ final KieSession kSession = getKieSession(runType, DRL);
+
+ // === PHASE 2: Event Listener Setup for Rule Execution Tracking ===
+
+ // Track which rules fire during execution for validation
+ final List<String> firedRules = new ArrayList<>();
+
+ // Add event listener to capture rule firing events
+ kSession.addEventListener(new DefaultAgendaEventListener() {
+ @Override
+ public void afterMatchFired(final AfterMatchFiredEvent event) {
+ // Record the name of each rule that fires
+ firedRules.add(event.getMatch().getRule().getName());
+ }
+ });
+
+ // Set the global variable that rules use to manipulate agenda focus
+ kSession.setGlobal("ksession", kSession);
+
+ // === PHASE 3: Fact Insertion - Setting Up the Test Scenario ===
+
+ // Insert the static fact with flag=true (required for Rule A to match)
+ // This fact will remain constant throughout the test
+ final FactWrapper staticFact = new FactWrapper("static");
+ staticFact.setFlag(true); // Pre-set to true so Rule A can
potentially match
+ kSession.insert(staticFact);
+
+ // Insert multiple dynamic facts with flag=false (triggers for Setup
Rule)
+ // These facts will be modified by the Setup Rule to flag=true
+ for (int i = 1; i <= 3; i++) {
+ // Each dynamic fact starts with flag=false, matching Setup Rule
conditions
+ kSession.insert(new FactWrapper("dynamic-%d".formatted(i)));
+ }
+
+ // === PHASE 4: Rule Execution - The Core Test Logic ===
+
+ // Set initial agenda focus to GroupA where Setup Rule and Rule A
reside
+ kSession.getAgenda().getAgendaGroup("GroupA").setFocus();
+
+ // Execute all rules - this is where the complex interaction happens:
+ // 1. Setup Rule fires for each dynamic fact (3 times)
+ // 2. Each Setup Rule execution modifies a dynamic fact and switches
to GroupB
+ // 3. Rule B fires in GroupB for each modified dynamic fact (3 times)
+ // 4. Rule A never fires despite having matching conditions
(activation group prevents it)
+ kSession.fireAllRules();
+
+ // === PHASE 5: Validation - Verify Expected Behavior ===
+
+ final boolean ruleAFired = firedRules.contains("Rule A - GroupA");
+ final long ruleBFires = firedRules.stream().filter(r -> r.equals("Rule
B - GroupB")).count();
+ final long setupFires = firedRules.stream().filter(r ->
r.equals("Setup Rule - GroupA")).count();
+
+ // Validate the expected execution pattern
+ assertThat(setupFires).as("Setup rule should fire 3 times (once per
dynamic fact).").isEqualTo(3);
+ assertThat(ruleBFires).as("Rule B should fire 3 times (once per setup
cycle).").isEqualTo(3);
+ assertThat(ruleAFired).as("Rule A should NOT have fired due to
activation group mutual exclusion.").isFalse();
+
+ // Clean up resources
+ kSession.dispose();
+ }
+}
\ No newline at end of file
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]