This is an automated email from the ASF dual-hosted git repository.
yesamer 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 13f612ea665 fix(drools): accumulate rule stops firing after its
segment is split (#7110)
13f612ea665 is described below
commit 13f612ea665f2a9cef6ba8457686ba4152ee7f6b
Author: Daniel Clark <[email protected]>
AuthorDate: Wed Sep 16 11:24:20 2026 +0200
fix(drools): accumulate rule stops firing after its segment is split (#7110)
---
.../java/org/drools/core/reteoo/SegmentMemory.java | 15 ++
.../SegmentSplitAccumulateTest.java | 271 +++++++++++++++++++++
2 files changed, 286 insertions(+)
diff --git
a/drools-core/src/main/java/org/drools/core/reteoo/SegmentMemory.java
b/drools-core/src/main/java/org/drools/core/reteoo/SegmentMemory.java
index 2d2c82f4ddd..a909328c292 100644
--- a/drools-core/src/main/java/org/drools/core/reteoo/SegmentMemory.java
+++ b/drools-core/src/main/java/org/drools/core/reteoo/SegmentMemory.java
@@ -1202,6 +1202,21 @@ public class SegmentMemory extends
LinkedList<SegmentMemory>
public AccumulateMemoryPrototype(BetaMemoryPrototype betaProto) {
this.betaProto = betaProto;
+ this.nodePosMaskBit = betaProto.getNodePosMaskBit();
+ }
+
+ @Override
+ public void setNodePosMaskBit(long nodePosMaskBit) {
+ // the bit lives in the wrapped BetaMemoryPrototype, which
populateMemory() copies into the BetaMemory:
+ // keep both in sync, otherwise a segment split
(SegmentPrototype.splitProtos) renumbers only this
+ // wrapper and the accumulate node's BetaMemory keeps its
pre-split position bit
+ super.setNodePosMaskBit(nodePosMaskBit);
+ betaProto.setNodePosMaskBit(nodePosMaskBit);
+ }
+
+ @Override
+ public long getNodePosMaskBit() {
+ return betaProto.getNodePosMaskBit();
}
@Override
diff --git
a/drools-test-coverage/test-compiler-integration/src/test/java/org/drools/mvel/integrationtests/SegmentSplitAccumulateTest.java
b/drools-test-coverage/test-compiler-integration/src/test/java/org/drools/mvel/integrationtests/SegmentSplitAccumulateTest.java
new file mode 100644
index 00000000000..5a4588418ae
--- /dev/null
+++
b/drools-test-coverage/test-compiler-integration/src/test/java/org/drools/mvel/integrationtests/SegmentSplitAccumulateTest.java
@@ -0,0 +1,271 @@
+/*
+ * 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;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Stream;
+
+import org.drools.base.base.ClassObjectType;
+import org.drools.core.common.InternalWorkingMemory;
+import org.drools.core.impl.InternalRuleBase;
+import org.drools.core.reteoo.AccumulateNode;
+import org.drools.core.reteoo.AccumulateNode.AccumulateMemory;
+import org.drools.core.reteoo.BetaMemory;
+import org.drools.core.reteoo.LeftInputAdapterNode;
+import org.drools.core.reteoo.LeftTupleSink;
+import org.drools.core.reteoo.ObjectTypeNode;
+import org.drools.core.reteoo.SegmentMemory;
+import org.drools.testcoverage.common.model.A;
+import org.drools.testcoverage.common.model.B;
+import org.drools.testcoverage.common.model.C;
+import org.drools.testcoverage.common.util.KieBaseTestConfiguration;
+import org.drools.testcoverage.common.util.KieBaseUtil;
+import org.drools.testcoverage.common.util.TestParametersUtil2;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.kie.api.KieBase;
+import org.kie.api.runtime.KieSession;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * A rule whose path contains an AccumulateNode stops firing when its segment
is split by a rule that is
+ * added later and shares the same LeftInputAdapterNode.
+ *
+ * <p>SegmentPrototype.splitProtos() renumbers the node position bits of the
second half of a split segment
+ * by calling MemoryPrototype.setNodePosMaskBit() on each memory prototype.
AccumulateMemoryPrototype holds
+ * the bit in a wrapped BetaMemoryPrototype and its populateMemory() copies
that wrapped bit into the
+ * BetaMemory, so a renumbering applied to the wrapper alone never reaches the
accumulate node and the node
+ * is created with its pre-split bit.</p>
+ *
+ * <p>At runtime the accumulate node then links and dirties the bit of the
node that follows it, and
+ * SegmentCursor.moveToNextAvailableSegment() skips nodes whose dirty bit is
clear, so the staged right
+ * tuples of the accumulate node are never processed and the rule never
fires.</p>
+ *
+ * <p>Two packages are used because KnowledgeBaseImpl.addPackages() sorts
packages by rule count descending
+ * and adds them one at a time: the first package gets its segment prototypes
created in bulk once all of
+ * its rules are attached, while every later package is added rule by rule
through
+ * ReteooRuleBuilder.attachTerminalNode() and PhreakBuilder.addRule(), which
is the only path that splits an
+ * existing segment.</p>
+ */
+public class SegmentSplitAccumulateTest {
+
+ public static Stream<KieBaseTestConfiguration> parameters() {
+ return
TestParametersUtil2.getKieBaseCloudConfigurations(true).stream();
+ }
+
+ private static final String HEADER =
+ "package org.drools.mvel.integrationtests.segmentsplit;\n" +
+ "import " + A.class.getCanonicalName() + ";\n" +
+ "import " + B.class.getCanonicalName() + ";\n" +
+ "import " + C.class.getCanonicalName() + ";\n" +
+ "global java.util.List results;\n";
+
+ private static final String RULE_ACCUMULATE_FROM =
+ "rule \"R1 accumulate then from\"\n" +
+ "when\n" +
+ " A( $v : value )\n" +
+ " accumulate( $b : B( value == $v ); $list : collectList( $b );
$list.size > 0 )\n" +
+ " B( $r : value ) from $list.get(0)\n" +
+ "then\n" +
+ " results.add( \"R1:\" + $r );\n" +
+ "end\n";
+
+ private static final String RULE_ACCUMULATE_ONLY =
+ "rule \"R1 accumulate\"\n" +
+ "when\n" +
+ " A( $v : value )\n" +
+ " accumulate( B( value == $v ); $n : count(); $n > 0 )\n" +
+ "then\n" +
+ " results.add( \"R1:\" + $n );\n" +
+ "end\n";
+
+ private static final String RULE_JOIN =
+ "rule \"R1 join\"\n" +
+ "when\n" +
+ " A( $v : value )\n" +
+ " B( value == $v, $r : value )\n" +
+ "then\n" +
+ " results.add( \"R1:\" + $r );\n" +
+ "end\n";
+
+ private static final String RULE_SHARING_LIA =
+ "rule \"R2 shares the A pattern\"\n" +
+ "when\n" +
+ " A( $v : value )\n" +
+ " C( value == $v )\n" +
+ "then\n" +
+ " results.add( \"R2:\" + $v );\n" +
+ "end\n";
+
+ /** A second package with more rules, so that it is built first and the
package under test is split. */
+ private static final String BIGGER_PACKAGE =
+ "package org.drools.mvel.integrationtests.segmentsplit.other;\n" +
+ "import " + C.class.getCanonicalName() + ";\n" +
+ "rule \"O1\" when C( value == 101 ) then end\n" +
+ "rule \"O2\" when C( value == 102 ) then end\n" +
+ "rule \"O3\" when C( value == 103 ) then end\n";
+
+ @ParameterizedTest(name = "KieBase type={0}")
+ @MethodSource("parameters")
+ public void
accumulateAndFromRuleBuiltBeforeSharingRule(KieBaseTestConfiguration
kieBaseTestConfiguration) {
+ assertRuleFiresWhenRightFactArrivesLater(kieBaseTestConfiguration,
+ HEADER + RULE_ACCUMULATE_FROM
+ RULE_SHARING_LIA, "R1:1");
+ }
+
+ @ParameterizedTest(name = "KieBase type={0}")
+ @MethodSource("parameters")
+ public void
accumulateOnlyRuleBuiltBeforeSharingRule(KieBaseTestConfiguration
kieBaseTestConfiguration) {
+ assertRuleFiresWhenRightFactArrivesLater(kieBaseTestConfiguration,
+ HEADER + RULE_ACCUMULATE_ONLY
+ RULE_SHARING_LIA, "R1:1");
+ }
+
+ @ParameterizedTest(name = "KieBase type={0}")
+ @MethodSource("parameters")
+ public void
accumulateAndFromRuleBuiltAfterSharingRule(KieBaseTestConfiguration
kieBaseTestConfiguration) {
+ assertRuleFiresWhenRightFactArrivesLater(kieBaseTestConfiguration,
+ HEADER + RULE_SHARING_LIA +
RULE_ACCUMULATE_FROM, "R1:1");
+ }
+
+ @ParameterizedTest(name = "KieBase type={0}")
+ @MethodSource("parameters")
+ public void
accumulateOnlyRuleBuiltAfterSharingRule(KieBaseTestConfiguration
kieBaseTestConfiguration) {
+ assertRuleFiresWhenRightFactArrivesLater(kieBaseTestConfiguration,
+ HEADER + RULE_SHARING_LIA +
RULE_ACCUMULATE_ONLY, "R1:1");
+ }
+
+ @ParameterizedTest(name = "KieBase type={0}")
+ @MethodSource("parameters")
+ public void joinRuleBuiltBeforeSharingRule(KieBaseTestConfiguration
kieBaseTestConfiguration) {
+ assertRuleFiresWhenRightFactArrivesLater(kieBaseTestConfiguration,
+ HEADER + RULE_JOIN +
RULE_SHARING_LIA, "R1:1");
+ }
+
+ @ParameterizedTest(name = "KieBase type={0}")
+ @MethodSource("parameters")
+ public void
accumulateRuleWithAllFactsInsertedBeforeFirstFire(KieBaseTestConfiguration
kieBaseTestConfiguration) {
+ KieBase kbase = buildKieBase(kieBaseTestConfiguration, HEADER +
RULE_ACCUMULATE_FROM + RULE_SHARING_LIA);
+ KieSession ksession = kbase.newKieSession();
+ try {
+ List<String> results = new ArrayList<>();
+ ksession.setGlobal("results", results);
+ ksession.insert(new A(1));
+ ksession.insert(new B(1));
+ ksession.fireAllRules();
+ assertThat(results).containsExactly("R1:1");
+ } finally {
+ ksession.dispose();
+ }
+ }
+
+ /**
+ * A single package has all of its rules attached before its segment
prototypes are created in bulk, so
+ * no segment is ever split and the accumulate node keeps the bit it was
built with.
+ */
+ @ParameterizedTest(name = "KieBase type={0}")
+ @MethodSource("parameters")
+ public void
accumulateRuleBeforeSharingRuleInASinglePackage(KieBaseTestConfiguration
kieBaseTestConfiguration) {
+ KieBase kbase =
KieBaseUtil.getKieBaseFromKieModuleFromDrl("segment-split-single",
kieBaseTestConfiguration,
+ HEADER +
RULE_ACCUMULATE_FROM + RULE_SHARING_LIA);
+ KieSession ksession = kbase.newKieSession();
+ try {
+ List<String> results = new ArrayList<>();
+ ksession.setGlobal("results", results);
+ ksession.insert(new A(1));
+ ksession.fireAllRules();
+ ksession.insert(new B(1));
+ ksession.fireAllRules();
+ assertThat(results).containsExactly("R1:1");
+ } finally {
+ ksession.dispose();
+ }
+ }
+
+ /**
+ * The accumulate node is the root of the segment split off by the rule
sharing the LeftInputAdapterNode,
+ * so its position bit within that segment has to be the first one.
+ */
+ @ParameterizedTest(name = "KieBase type={0}")
+ @MethodSource("parameters")
+ public void
accumulateNodeKeepsItsPositionBitAfterSegmentSplit(KieBaseTestConfiguration
kieBaseTestConfiguration) {
+ KieBase kbase = buildKieBase(kieBaseTestConfiguration, HEADER +
RULE_ACCUMULATE_FROM + RULE_SHARING_LIA);
+
+ ObjectTypeNode aotn = getObjectTypeNode(kbase, A.class);
+ LeftInputAdapterNode liaNode = (LeftInputAdapterNode)
aotn.getObjectSinkPropagator().getSinks()[0];
+ AccumulateNode accNode = null;
+ for (LeftTupleSink sink : liaNode.getSinkPropagator().getSinks()) {
+ if (sink instanceof AccumulateNode) {
+ accNode = (AccumulateNode) sink;
+ }
+ }
+ assertThat(accNode).as("the accumulate node shares the
LeftInputAdapterNode").isNotNull();
+
+ KieSession ksession = kbase.newKieSession();
+ try {
+ ksession.setGlobal("results", new ArrayList<String>());
+ ksession.insert(new A(1));
+ ksession.fireAllRules();
+
+ InternalWorkingMemory wm = (InternalWorkingMemory) ksession;
+ BetaMemory bm = ((AccumulateMemory)
wm.getNodeMemory(accNode)).getBetaMemory();
+ SegmentMemory smem = bm.getSegmentMemory();
+
+ assertThat(smem.getRootNode()).as("the split made the accumulate
node a segment root").isEqualTo(accNode);
+ assertThat(bm.getNodePosMaskBit()).as("position bit of the first
node of the segment").isEqualTo(1L);
+ } finally {
+ ksession.dispose();
+ }
+ }
+
+ private void
assertRuleFiresWhenRightFactArrivesLater(KieBaseTestConfiguration
kieBaseTestConfiguration,
+ String drl, String
expected) {
+ KieSession ksession = buildKieBase(kieBaseTestConfiguration,
drl).newKieSession();
+ try {
+ List<String> results = new ArrayList<>();
+ ksession.setGlobal("results", results);
+
+ // A(1) reaches the accumulate node while no B exists, so the
result constraint is not satisfied
+ ksession.insert(new A(1));
+ ksession.fireAllRules();
+ assertThat(results).as("nothing may fire before B(1)
exists").isEmpty();
+
+ // B(1) arrives on the right input of the accumulate node
+ ksession.insert(new B(1));
+ ksession.fireAllRules();
+ assertThat(results).as("R1 must fire once B(1)
exists").containsExactly(expected);
+ } finally {
+ ksession.dispose();
+ }
+ }
+
+ private KieBase buildKieBase(KieBaseTestConfiguration
kieBaseTestConfiguration, String drl) {
+ return KieBaseUtil.getKieBaseFromKieModuleFromDrl("segment-split",
kieBaseTestConfiguration,
+ BIGGER_PACKAGE, drl);
+ }
+
+ private ObjectTypeNode getObjectTypeNode(KieBase kbase, Class<?>
nodeClass) {
+ for (ObjectTypeNode n : ((InternalRuleBase)
kbase).getRete().getObjectTypeNodes()) {
+ if (((ClassObjectType) n.getObjectType()).getClassType() ==
nodeClass) {
+ return n;
+ }
+ }
+ return null;
+ }
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]