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.git
The following commit(s) were added to refs/heads/main by this push:
new f81eb19a6b9 [incubator-kie#7209] Ignore @Propagation(IMMEDIATE) in
sequential mode (#7220)
f81eb19a6b9 is described below
commit f81eb19a6b97403c950e96c03f3cc63384a450b7
Author: Toshiya Kobayashi <[email protected]>
AuthorDate: Fri Oct 23 16:32:04 2026 +0900
[incubator-kie#7209] Ignore @Propagation(IMMEDIATE) in sequential mode
(#7220)
Data-driven evaluation bypasses the agenda and evaluates paths as soon
as segments link. In sequential mode tuple memory is disabled, so
this early evaluation hits null tuple memory causing NPE or incorrect
results.
Disable data-driven flag at runtime in PathMemory.isRuleDataDriven
when the session is sequential, and log a WARN in ReteooRuleBuilder
when a data-driven rule is added to a sequential kbase.
Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
---
.../java/org/drools/core/reteoo/PathMemory.java | 4 +
.../core/reteoo/builder/ReteooRuleBuilder.java | 8 +
.../sequential/SequentialPropagationTest.java | 207 +++++++++++++++++++++
3 files changed, 219 insertions(+)
diff --git a/drools-core/src/main/java/org/drools/core/reteoo/PathMemory.java
b/drools-core/src/main/java/org/drools/core/reteoo/PathMemory.java
index 1745e58b7ac..42fc5aafb4f 100644
--- a/drools-core/src/main/java/org/drools/core/reteoo/PathMemory.java
+++ b/drools-core/src/main/java/org/drools/core/reteoo/PathMemory.java
@@ -62,6 +62,10 @@ public class PathMemory extends
AbstractLinkedListNode<Memory>
}
protected boolean isRuleDataDriven(ReteEvaluator reteEvaluator, RuleImpl
rule) {
+ // Data-driven evaluation in sequential mode causes NPE and incorrect
results (incubator-kie#7209)
+ if (reteEvaluator != null && reteEvaluator.isSequential()) {
+ return false;
+ }
return rule != null &&
(rule.isDataDriven() || (reteEvaluator != null &&
reteEvaluator.getRuleSessionConfiguration()
.getForceEagerActivationFilter().accept(rule)));
diff --git
a/drools-core/src/main/java/org/drools/core/reteoo/builder/ReteooRuleBuilder.java
b/drools-core/src/main/java/org/drools/core/reteoo/builder/ReteooRuleBuilder.java
index e6f47d468a2..364aee32e39 100755
---
a/drools-core/src/main/java/org/drools/core/reteoo/builder/ReteooRuleBuilder.java
+++
b/drools-core/src/main/java/org/drools/core/reteoo/builder/ReteooRuleBuilder.java
@@ -57,9 +57,13 @@ import org.drools.core.reteoo.TerminalNode;
import org.drools.core.reteoo.WindowNode;
import org.drools.core.time.TemporalDependencyMatrix;
import org.kie.api.conf.EventProcessingOption;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
public class ReteooRuleBuilder implements RuleBuilder {
+ private static final Logger log =
LoggerFactory.getLogger(ReteooRuleBuilder.class);
+
protected BuildUtils utils;
public ReteooRuleBuilder() {
@@ -120,6 +124,10 @@ public class ReteooRuleBuilder implements RuleBuilder {
// transform rule and gets the array of subrules
final GroupElement[] subrules = rule.getTransformedLhs(
LogicTransformer.getInstance(), kBase.getGlobals() );
+ if (kBase.getRuleBaseConfiguration().isSequential() &&
rule.isDataDriven()) {
+ log.warn("@Propagation(IMMEDIATE) on rule '{}' is ignored in
sequential mode", rule.getName());
+ }
+
for (int i = 0; i < subrules.length; i++) {
// creates a clean build context for each subrule
final BuildContext context = new BuildContext( kBase,
workingMemories );
diff --git
a/drools-test-coverage/test-compiler-integration/src/test/java/org/drools/mvel/integrationtests/sequential/SequentialPropagationTest.java
b/drools-test-coverage/test-compiler-integration/src/test/java/org/drools/mvel/integrationtests/sequential/SequentialPropagationTest.java
new file mode 100644
index 00000000000..aab5b2fb369
--- /dev/null
+++
b/drools-test-coverage/test-compiler-integration/src/test/java/org/drools/mvel/integrationtests/sequential/SequentialPropagationTest.java
@@ -0,0 +1,207 @@
+/*
+ * 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.mvel.integrationtests.sequential;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Stream;
+
+import org.drools.mvel.compiler.Address;
+import org.drools.mvel.compiler.Cheese;
+import org.drools.mvel.compiler.Person;
+import org.drools.testcoverage.common.util.KieBaseTestConfiguration;
+import org.drools.testcoverage.common.util.KieBaseUtil;
+import org.drools.testcoverage.common.util.KieUtil;
+import org.drools.testcoverage.common.util.TestParametersUtil2;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.kie.api.KieBase;
+import org.kie.api.builder.KieModule;
+import org.kie.api.conf.SequentialOption;
+import org.kie.api.runtime.StatelessKieSession;
+import org.kie.internal.command.CommandFactory;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Tests that {@code @Propagation(IMMEDIATE)} is safely ignored in sequential
mode,
+ * and that {@code @Propagation(EAGER)} continues to work correctly.
+ *
+ * <p>{@code @Propagation(IMMEDIATE)} sets {@code RuleImpl.dataDriven = true},
which
+ * causes the path to be evaluated as soon as a segment links, bypassing the
agenda.
+ * In sequential mode tuple memory is disabled, so this early evaluation hits
null
+ * tuple memory and either throws NPE or produces incorrect results.</p>
+ *
+ * <p>R2 uses {@code Person( age >= 0 )} so that a modify on age is delivered
as an
+ * <em>update</em> (not an insert) to R2's beta node, which is the condition
that
+ * triggers the NPE. A constraint like {@code Person()} would not react to age
changes,
+ * and {@code Person( age == 1 )} would not match before the modify.</p>
+ *
+ * @see <a
href="https://github.com/apache/incubator-kie/issues/7209">incubator-kie#7209</a>
+ */
+public class SequentialPropagationTest {
+
+ public static Stream<Arguments> parameters() {
+ List<Arguments> args = new ArrayList<>();
+ for (KieBaseTestConfiguration config :
TestParametersUtil2.getKieBaseCloudConfigurations(true)) {
+ args.add(Arguments.of(config, "IMMEDIATE"));
+ args.add(Arguments.of(config, "EAGER"));
+ }
+ return args.stream();
+ }
+
+ /**
+ * Single rule with the propagation annotation and no modify.
+ * Without the fix, IMMEDIATE causes the rule not to fire at all.
+ */
+ @ParameterizedTest(name = "KieBase type={0}, propagation={1}")
+ @MethodSource("parameters")
+ public void testNoModify(KieBaseTestConfiguration
kieBaseTestConfiguration, String propagation) {
+ String str =
+ "package org.drools.mvel.compiler.test\n" +
+ "import " + Person.class.getCanonicalName() + "\n" +
+ "import " + Cheese.class.getCanonicalName() + "\n" +
+ "global " + List.class.getCanonicalName() + " list\n" +
+ "rule R @Propagation(" + propagation + ") when\n" +
+ " $p : Person()\n" +
+ " $c : Cheese( type == $p.likes )\n" +
+ "then\n" +
+ " list.add( \"R\" );\n" +
+ "end\n";
+
+ Person p = new Person("p1", "stilton", 0);
+ Cheese c = new Cheese("stilton", 10);
+ List<String> list = executeSequential(str, kieBaseTestConfiguration,
p, c);
+ assertThat(list).containsExactly("R");
+ }
+
+ /**
+ * JoinNode left update: R1 modifies Person.age, R2 has {@code Person( age
>= 0 )} joined with Cheese.
+ * Without the fix, IMMEDIATE causes NPE on null left tuple memory.
+ */
+ @ParameterizedTest(name = "KieBase type={0}, propagation={1}")
+ @MethodSource("parameters")
+ public void testJoinNodeLeftUpdate(KieBaseTestConfiguration
kieBaseTestConfiguration, String propagation) {
+ String str =
+ "package org.drools.mvel.compiler.test\n" +
+ "import " + Person.class.getCanonicalName() + "\n" +
+ "import " + Cheese.class.getCanonicalName() + "\n" +
+ "global " + List.class.getCanonicalName() + " list\n" +
+ "rule R1 salience 100 when\n" +
+ " $p : Person()\n" +
+ " $c : Cheese( type == $p.likes )\n" +
+ "then\n" +
+ " modify( $p ) { setAge( 1 ) };\n" +
+ " list.add( \"R1\" );\n" +
+ "end\n" +
+ "rule R2 @Propagation(" + propagation + ") when\n" +
+ " $p : Person( age >= 0 )\n" +
+ " $c : Cheese( type == $p.likes )\n" +
+ "then\n" +
+ " list.add( \"R2:\" + $p.getAge() );\n" +
+ "end\n";
+
+ Person p = new Person("p1", "stilton", 0);
+ Cheese c = new Cheese("stilton", 10);
+ List<String> list = executeSequential(str, kieBaseTestConfiguration,
p, c);
+ assertThat(list).containsExactly("R1", "R2:1");
+ }
+
+ /**
+ * NotNode right update: R1 modifies Cheese.type to "cheddar", which
should block
+ * R2's {@code not Cheese( type == "cheddar" )}.
+ * Without the fix, IMMEDIATE causes R2 to fire incorrectly.
+ */
+ @ParameterizedTest(name = "KieBase type={0}, propagation={1}")
+ @MethodSource("parameters")
+ public void testNotNodeRightUpdate(KieBaseTestConfiguration
kieBaseTestConfiguration, String propagation) {
+ String str =
+ "package org.drools.mvel.compiler.test\n" +
+ "import " + Person.class.getCanonicalName() + "\n" +
+ "import " + Cheese.class.getCanonicalName() + "\n" +
+ "global " + List.class.getCanonicalName() + " list\n" +
+ "rule R1 salience 100 when\n" +
+ " $p : Person()\n" +
+ " $c : Cheese( type == \"stilton\" )\n" +
+ "then\n" +
+ " modify( $c ) { setType( \"cheddar\" ) };\n" +
+ " list.add( \"R1\" );\n" +
+ "end\n" +
+ "rule R2 @Propagation(" + propagation + ") when\n" +
+ " $p : Person( age >= 0 )\n" +
+ " not Cheese( type == \"cheddar\" )\n" +
+ "then\n" +
+ " list.add( \"R2\" );\n" +
+ "end\n";
+
+ Person p = new Person("p1", "stilton", 0);
+ Cheese c = new Cheese("stilton", 10);
+ List<String> list = executeSequential(str, kieBaseTestConfiguration,
p, c);
+ assertThat(list).containsExactly("R1");
+ }
+
+ /**
+ * FromNode left update: R1 changes address to Paris and modifies
Person.age.
+ * R2 has {@code Person( age >= 0 )} with {@code Address() from
$p.addresses}.
+ * Without the fix, IMMEDIATE causes NPE on null previousMatches.
+ */
+ @ParameterizedTest(name = "KieBase type={0}, propagation={1}")
+ @MethodSource("parameters")
+ public void testFromNodeLeftUpdate(KieBaseTestConfiguration
kieBaseTestConfiguration, String propagation) {
+ String str =
+ "package org.drools.mvel.compiler.test\n" +
+ "import " + Person.class.getCanonicalName() + "\n" +
+ "import " + Address.class.getCanonicalName() + "\n" +
+ "global " + List.class.getCanonicalName() + " list\n" +
+ "rule R1 salience 100 when\n" +
+ " $p : Person()\n" +
+ " $a : Address( street == \"London\" ) from $p.addresses\n"
+
+ "then\n" +
+ " $a.setStreet( \"Paris\" );\n" +
+ " modify( $p ) { setAge( 1 ) };\n" +
+ " list.add( \"R1\" );\n" +
+ "end\n" +
+ "rule R2 @Propagation(" + propagation + ") when\n" +
+ " $p : Person( age >= 0 )\n" +
+ " $a : Address() from $p.addresses\n" +
+ "then\n" +
+ " list.add( \"R2:\" + $a.getStreet() );\n" +
+ "end\n";
+
+ Person p = new Person("p1", "stilton", 0);
+ p.addAddress(new Address("London"));
+ List<String> list = executeSequential(str, kieBaseTestConfiguration,
p);
+ assertThat(list).containsExactly("R1", "R2:Paris");
+ }
+
+ private List<String> executeSequential(String drl,
KieBaseTestConfiguration kieBaseTestConfiguration, Object... facts) {
+ final KieModule kieModule = KieUtil.getKieModuleFromDrls("test",
kieBaseTestConfiguration, drl);
+ final KieBase kbase =
KieBaseUtil.newKieBaseFromKieModuleWithAdditionalOptions(
+ kieModule, kieBaseTestConfiguration, SequentialOption.YES);
+ StatelessKieSession ksession = kbase.newStatelessKieSession();
+
+ final List<String> list = new ArrayList<>();
+ ksession.setGlobal("list", list);
+
+
ksession.execute(CommandFactory.newInsertElements(Arrays.asList(facts)));
+ return list;
+ }
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]