You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@kie.apache.org by ri...@apache.org on 2026/09/14 07:36:19 UTC
(incubator-kie) branch main updated: feat(sequencing): introduce sequence() DSL and Phreak SequenceNode (#6909)
This is an automated email from the ASF dual-hosted git repository.
Rikkola 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 d97dd8bd20b feat(sequencing): introduce sequence() DSL and Phreak SequenceNode (#6909)
d97dd8bd20b is described below
commit d97dd8bd20b5caf046939dd58ec3307b9b2b77d7
Author: Toni Rikkola <to...@rikkola.net>
AuthorDate: Mon Sep 14 10:36:14 2026 +0300
feat(sequencing): introduce sequence() DSL and Phreak SequenceNode (#6909)
Adds a new rule condition type that matches an ordered sequence of events.
New runtime infrastructure (drools-base, drools-core):
- Sequence / Sequencer / SequencerMemory: data model for a named sequence
of steps; each step is a LogicCircuit of AND/OR gates over alpha-filtered
event slots
- SignalProcessor hierarchy (LogicGate, TerminatingSignalProcessor, etc.):
bit-mask signal propagation that advances or terminates a sequence on
each matching fact
- DynamicFilter / DynamicFilterProto / SignalAdapter: per-session filters
that attach signal adapters to the active alpha network while a step is
live
- SequenceNode (LeftTupleSource): new Rete/Phreak node that hosts the
sequencer; SequencerMemoryImpl holds per-left-tuple sequencer state
- SequenceBuilder / PhreakNodeFactory: build-time wiring of SequenceNode
into the Rete network; CombinedAlphaConstraint aggregates per-step
alpha constraints into a single filter slot
Canonical model DSL (drools-canonical-model):
- PatternDSL.sequence() entry point; SequenceViewItem and SequenceStep
marker interface; SequenceConditionImpl (Condition.Type.SEQUENCE)
- PatternDSL.or() / and() overloads for use inside sequence steps
- PatternDef now extends SequenceStep so patterns compose directly as steps
Compiler integration (drools-model-compiler):
- KiePackagesBuilder: translate SequenceConditionImpl → Sequence/Sequencer
and wire the SequenceNode during rule compilation
- LambdaConsequence: supply sequencer fact-handles to rule consequences
so matched facts are accessible from RHS code
Fixes: apache/incubator-kie#7059
Co-authored-by: Toni Rikkola <to...@ibm.com>
---
.../base/definitions/rule/impl/RuleImpl.java | 6 +
.../java/org/drools/base/reteoo/DynamicFilter.java | 87 ++++
.../org/drools/base/reteoo/DynamicFilterProto.java | 33 +-
.../java/org/drools/base/reteoo/NodeTypeEnums.java | 1 +
.../java/org/drools/base/reteoo/SignalAdapter.java | 48 ++
.../drools/base/reteoo/sequencing/Sequence.java | 328 ++++++++++++
.../drools/base/reteoo/sequencing/Sequencer.java | 70 +++
.../base/reteoo/sequencing/SequencerMemory.java | 47 ++
.../reteoo/sequencing/signalprocessors/Gates.java | 30 +-
.../sequencing/signalprocessors/LogicCircuit.java | 29 +-
.../sequencing/signalprocessors/LogicGate.java | 123 +++++
.../LogicGateOutputSignalProcessor.java | 49 ++
.../sequencing/signalprocessors/SignalIndex.java | 33 +-
.../signalprocessors/SignalProcessor.java | 31 +-
.../TerminatingSignalProcessor.java | 46 +-
.../base/reteoo/sequencing/steps/AbstractStep.java | 30 +-
.../reteoo/sequencing/steps/LogicCircuitStep.java | 50 ++
.../drools/base/reteoo/sequencing/steps/Step.java | 49 ++
.../rule/constraint/CombinedAlphaConstraint.java | 110 +++++
.../org/drools/base/util/CircularArrayList.java | 128 +++++
.../java/org/drools/base}/util/LinkedListTest.java | 11 +-
.../org/drools/core/common/SuperCacheFixer.java | 5 +
.../core/impl/WorkingMemoryReteExpireAction.java | 6 +-
.../core/phreak/BuildtimeSegmentUtilities.java | 11 +
.../core/phreak/PhreakNetworkNodeFactory.java | 3 +
.../core/phreak/PhreakNetworkNodeFactoryImpl.java | 7 +
.../org/drools/core/phreak/PhreakTimerNode.java | 1 -
.../core/phreak/RuleNetworkEvaluatorImpl.java | 10 +
.../drools/core/reteoo/RightInputAdapterNode.java | 5 +-
.../java/org/drools/core/reteoo/SegmentMemory.java | 13 +
.../java/org/drools/core/reteoo/SequenceNode.java | 550 +++++++++++++++++++++
.../java/org/drools/core/reteoo/TupleFactory.java | 6 +
.../drools/core/reteoo/builder/NodeFactory.java | 6 +
.../core/reteoo/builder/PhreakNodeFactory.java | 94 +++-
.../core/reteoo/builder/ReteooRuleBuilder.java | 3 +
.../core/reteoo/builder/SequenceBuilder.java | 55 +++
.../reteoo/sequencing/SequencerMemoryImpl.java | 152 ++++++
drools-metric/pom.xml | 5 +
.../phreak/MetricPhreakNetworkNodeFactoryImpl.java | 10 +
.../metric/phreak/PhreakSequenceNodeMetric.java | 51 ++
.../org/drools/metric/SequenceNodeMetricTest.java | 126 +++++
.../src/main/java/org/drools/model/Condition.java | 2 +-
.../src/main/java/org/drools/model/PatternDSL.java | 47 +-
...stentialExprViewItem.java => SequenceStep.java} | 34 +-
.../org/drools/model/impl/ViewPatternBuilder.java | 11 +
.../SequenceConditionImpl.java} | 28 +-
.../drools/model/view/CombinedExprViewItem.java | 2 +-
.../drools/model/view/ExistentialExprViewItem.java | 2 +-
.../model/codegen/execmodel/ReteDumperTest.java | 4 +-
.../drools/modelcompiler/KiePackagesBuilder.java | 129 ++++-
.../java/org/drools/modelcompiler/RuleContext.java | 28 ++
.../consequence/LambdaConsequence.java | 146 +++++-
.../PatternDSLSequenceAdvancedTest.java | 518 +++++++++++++++++++
.../PatternDSLSequenceCompositeTest.java | 335 +++++++++++++
.../PatternDSLSequenceConsequenceTest.java | 452 +++++++++++++++++
.../PatternDSLSequenceEdgeCaseTest.java | 514 +++++++++++++++++++
.../PatternDSLSequenceLifecycleTest.java | 298 +++++++++++
.../modelcompiler/PatternDSLSequenceTest.java | 157 ++++++
.../modelcompiler/SequenceReviewProbeTest.java | 288 +++++++++++
.../drools/modelcompiler/domain/SensorEvents.java | 120 +++++
.../impl/sessions/RuleUnitExecutorImpl.java | 2 +-
.../AbstractPhreakSequencerSubsequenceTest.java | 183 +++++++
.../phreak/sequencing/AlphaConstraint.java | 73 +++
.../integrationtests/phreak/sequencing/BEvent.java | 33 +-
.../integrationtests/phreak/sequencing/CEvent.java | 33 +-
.../phreak/sequencing/CircularArrayListTest.java | 91 ++++
.../integrationtests/phreak/sequencing/DEvent.java | 80 +++
.../integrationtests/phreak/sequencing/EEvent.java | 33 +-
.../phreak/sequencing/GatesTest.java | 67 +++
.../PhreakSequencerActivateDeactivateStepTest.java | 161 ++++++
.../sequencing/PhreakSequencerLogicGateTest.java | 156 ++++++
.../phreak/sequencing/Predicate1.java | 31 +-
.../phreak/sequencing/SequenceCloneTest.java | 65 +++
.../phreak/sequencing/TestMemories.java | 109 ++++
74 files changed, 6367 insertions(+), 323 deletions(-)
diff --git a/drools-base/src/main/java/org/drools/base/definitions/rule/impl/RuleImpl.java b/drools-base/src/main/java/org/drools/base/definitions/rule/impl/RuleImpl.java
index 1fb0fe6aed8..f290d39f7b2 100644
--- a/drools-base/src/main/java/org/drools/base/definitions/rule/impl/RuleImpl.java
+++ b/drools-base/src/main/java/org/drools/base/definitions/rule/impl/RuleImpl.java
@@ -37,6 +37,7 @@ import org.drools.base.base.EnabledBoolean;
import org.drools.base.base.SalienceInteger;
import org.drools.base.base.ValueResolver;
import org.drools.base.reteoo.BaseTuple;
+import org.drools.base.reteoo.sequencing.Sequence;
import org.drools.base.rule.ConsequenceMetaData;
import org.drools.base.rule.Declaration;
import org.drools.base.rule.Dialectable;
@@ -545,6 +546,11 @@ public class RuleImpl implements Externalizable,
this.lhsRoot.addChild( element );
}
+ public void addSequence(final Sequence element) {
+ this.dirty = true;
+ this.lhsRoot.addChild( element );
+ }
+
/**
* Retrieve the <code>List</code> of <code>Conditions</code> for this
* rule.
diff --git a/drools-base/src/main/java/org/drools/base/reteoo/DynamicFilter.java b/drools-base/src/main/java/org/drools/base/reteoo/DynamicFilter.java
new file mode 100644
index 00000000000..498fc9faa87
--- /dev/null
+++ b/drools-base/src/main/java/org/drools/base/reteoo/DynamicFilter.java
@@ -0,0 +1,87 @@
+/*
+ * 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.base.reteoo;
+
+import org.drools.base.base.ValueResolver;
+import org.drools.base.rule.ContextEntry;
+import org.drools.base.rule.constraint.AlphaNodeFieldConstraint;
+import org.drools.base.rule.constraint.BetaConstraint;
+import org.drools.base.util.AbstractLinkedListNode;
+import org.drools.base.util.LinkedList;
+import org.kie.api.runtime.rule.FactHandle;
+
+public class DynamicFilter extends AbstractLinkedListNode<DynamicFilter> {
+ private AlphaNodeFieldConstraint constraint;
+ private LinkedList<SignalAdapter> signalAdapters;
+ private int adapterIndex;
+
+ public DynamicFilter(DynamicFilterProto proto) {
+ this.constraint = proto.getConstraint();
+ this.adapterIndex = proto.getAdapterIndex();
+ this.signalAdapters = new LinkedList<>();
+ }
+
+ public int getAdapterIndex() {
+ return adapterIndex;
+ }
+
+ public void addSignalAdapter(SignalAdapter signalAdapter) {
+ signalAdapters.add(signalAdapter);
+ }
+
+ public void removeSignalAdapter(SignalAdapter signalAdapter) {
+ signalAdapters.remove(signalAdapter);
+ }
+
+ public LinkedList<SignalAdapter> getSignalAdapters() {
+ return signalAdapters;
+ }
+
+ public void assertObject(final FactHandle factHandle,
+ final ValueResolver valueResolver) {
+
+ if (constraint.getRequiredDeclarations().length == 0) {
+ if (constraint.isAllowed(factHandle, valueResolver)) {
+ for (SignalAdapter current = signalAdapters.getFirst(); current != null; ) {
+ SignalAdapter next = current.getNext();
+ current.receive(valueResolver, factHandle);
+ current = next;
+ }
+ }
+ } else {
+ for (SignalAdapter current = signalAdapters.getFirst(); current != null; ) {
+ SignalAdapter next = current.getNext();
+ if (test(factHandle, valueResolver, current.getAnchorTuple())) {
+ current.receive(valueResolver, factHandle);
+ }
+ current = next;
+ }
+ }
+ }
+
+ private boolean test(FactHandle factHandle, ValueResolver valueResolver, BaseTuple tuple) {
+ if (constraint instanceof BetaConstraint) {
+ BetaConstraint<ContextEntry> betaConstraint = (BetaConstraint<ContextEntry>) constraint;
+ ContextEntry ctx = betaConstraint.createContext();
+ ctx.updateFromTuple(valueResolver, tuple);
+ return betaConstraint.isAllowedCachedLeft(ctx, factHandle);
+ }
+ throw new IllegalStateException("test() called with a non-beta constraint: " + constraint.getClass());
+ }
+}
diff --git a/drools-model/drools-canonical-model/src/main/java/org/drools/model/view/ExistentialExprViewItem.java b/drools-base/src/main/java/org/drools/base/reteoo/DynamicFilterProto.java
similarity index 54%
copy from drools-model/drools-canonical-model/src/main/java/org/drools/model/view/ExistentialExprViewItem.java
copy to drools-base/src/main/java/org/drools/base/reteoo/DynamicFilterProto.java
index d74396d1070..76737d347d7 100644
--- a/drools-model/drools-canonical-model/src/main/java/org/drools/model/view/ExistentialExprViewItem.java
+++ b/drools-base/src/main/java/org/drools/base/reteoo/DynamicFilterProto.java
@@ -16,33 +16,24 @@
* specific language governing permissions and limitations
* under the License.
*/
-package org.drools.model.view;
+package org.drools.base.reteoo;
-import org.drools.model.Condition;
-import org.drools.model.Variable;
+import org.drools.base.rule.constraint.AlphaNodeFieldConstraint;
-public class ExistentialExprViewItem<T> extends AbstractExprViewItem<T> {
+public class DynamicFilterProto {
+ private AlphaNodeFieldConstraint constraint;
+ private int adapterIndex;
- private final Condition.Type type;
- private final ViewItem expression;
-
- public ExistentialExprViewItem(Condition.Type type, ViewItem expression) {
- super(expression.getFirstVariable());
- this.type = type;
- this.expression = expression;
- }
-
- public ViewItem getExpression() {
- return expression;
+ public DynamicFilterProto(AlphaNodeFieldConstraint constraint, int adapterIndex) {
+ this.constraint = constraint;
+ this.adapterIndex = adapterIndex;
}
- @Override
- public Variable<?>[] getVariables() {
- return expression.getVariables();
+ public AlphaNodeFieldConstraint getConstraint() {
+ return constraint;
}
- @Override
- public Condition.Type getType() {
- return type;
+ public int getAdapterIndex() {
+ return adapterIndex;
}
}
diff --git a/drools-base/src/main/java/org/drools/base/reteoo/NodeTypeEnums.java b/drools-base/src/main/java/org/drools/base/reteoo/NodeTypeEnums.java
index 19bc533214a..67afcb4e260 100644
--- a/drools-base/src/main/java/org/drools/base/reteoo/NodeTypeEnums.java
+++ b/drools-base/src/main/java/org/drools/base/reteoo/NodeTypeEnums.java
@@ -75,6 +75,7 @@ public class NodeTypeEnums {
public static final int AsyncReceiveNode = (260 << shift) | TupleSourceMask | TupleSinkMask | TupleNodeMask | MemoryFactoryMask;
public static final int FromNode = (270 << shift) | TupleSourceMask | TupleSinkMask | TupleNodeMask | MemoryFactoryMask;
public static final int ReactiveFromNode = (280 << shift) | TupleSourceMask | TupleSinkMask | TupleNodeMask | MemoryFactoryMask;
+ public static final int SequenceNode = (290 << shift) | TupleSourceMask | TupleSinkMask | TupleNodeMask | MemoryFactoryMask;
public static final int QueryElementNode = (300 << shift) | TupleSourceMask | TupleSinkMask | TupleNodeMask | MemoryFactoryMask;
public static final int ConditionalBranchNode = (310 << shift) | TupleSourceMask | TupleSinkMask | TupleNodeMask | MemoryFactoryMask;
diff --git a/drools-base/src/main/java/org/drools/base/reteoo/SignalAdapter.java b/drools-base/src/main/java/org/drools/base/reteoo/SignalAdapter.java
new file mode 100644
index 00000000000..fef0f26f3a1
--- /dev/null
+++ b/drools-base/src/main/java/org/drools/base/reteoo/SignalAdapter.java
@@ -0,0 +1,48 @@
+/*
+ * 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.base.reteoo;
+
+import org.drools.base.base.ValueResolver;
+import org.drools.base.reteoo.sequencing.Sequence.SequenceMemory;
+import org.drools.base.reteoo.sequencing.signalprocessors.SignalProcessor;
+import org.drools.base.util.AbstractLinkedListNode;
+import org.kie.api.runtime.rule.FactHandle;
+
+public class SignalAdapter extends AbstractLinkedListNode<SignalAdapter> {
+ private SignalProcessor output;
+ private int signalBitIndex;
+ private int filterIndex;
+ private SequenceMemory memory;
+
+ public SignalAdapter(SignalProcessor output, int signalBitIndex, int filterIndex, SequenceMemory memory) {
+ this.output = output;
+ this.signalBitIndex = signalBitIndex;
+ this.filterIndex = filterIndex;
+ this.memory = memory;
+ }
+
+ public BaseTuple getAnchorTuple() {
+ return memory.getSequencerMemory().getLeftTuple();
+ }
+
+ public void receive(ValueResolver reteEvaluator, FactHandle factHandle) {
+ memory.setData(filterIndex, factHandle);
+ output.consume(signalBitIndex, memory, reteEvaluator);
+ }
+}
diff --git a/drools-base/src/main/java/org/drools/base/reteoo/sequencing/Sequence.java b/drools-base/src/main/java/org/drools/base/reteoo/sequencing/Sequence.java
new file mode 100644
index 00000000000..e9595e2c731
--- /dev/null
+++ b/drools-base/src/main/java/org/drools/base/reteoo/sequencing/Sequence.java
@@ -0,0 +1,328 @@
+/*
+ * 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.base.reteoo.sequencing;
+
+import org.drools.base.base.ValueResolver;
+import org.drools.base.reteoo.DynamicFilter;
+import org.drools.base.reteoo.sequencing.signalprocessors.LogicGate;
+import org.drools.base.reteoo.sequencing.steps.LogicCircuitStep;
+import org.drools.base.reteoo.sequencing.steps.Step;
+import org.drools.base.reteoo.sequencing.steps.Step.StepFactory;
+import org.drools.base.rule.Declaration;
+import org.drools.base.rule.Pattern;
+import org.drools.base.rule.RuleConditionElement;
+import org.drools.base.reteoo.SignalAdapter;
+import org.drools.base.util.CircularArrayList;
+import org.kie.api.runtime.rule.FactHandle;
+
+import java.io.IOException;
+import java.io.ObjectInput;
+import java.io.ObjectOutput;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+
+public class Sequence implements RuleConditionElement {
+ private final int sequenceIndex;
+
+ private Pattern[] filters;
+
+ private Step[] steps;
+
+ private LogicGate[] gates;
+
+ private SequenceController controller;
+
+ public Sequence(int sequenceIndex, StepFactory... stepFactories) {
+ this.steps = new Step[stepFactories.length];
+ this.sequenceIndex = sequenceIndex;
+ this.controller = DefaultController.getInstance();
+
+ for ( int i = 0; i < steps.length; i++ ) {
+ steps[i] = stepFactories[i].createStep(this);
+ }
+ populateLogicGates();
+ }
+
+ public Pattern[] getFilters() {
+ return filters;
+ }
+
+ public void setFilters(Pattern[] filters) {
+ this.filters = filters;
+ }
+
+ public void populateLogicGates() {
+ List<LogicGate> list = new ArrayList<>();
+ for (Step step : getSteps()) {
+ if (step instanceof LogicCircuitStep) {
+ LogicGate[] circuitGates = ((LogicCircuitStep) step).getCircuit().getGates();
+ for (int i = 0; i < circuitGates.length; i++) {
+ list.add(circuitGates[i]);
+ }
+ }
+ }
+ gates = list.toArray(new LogicGate[0]);
+ }
+
+ public int getSequenceIndex() {
+ return sequenceIndex;
+ }
+
+ public Step[] getSteps() {
+ return steps;
+ }
+
+ public LogicGate[] getGates() {
+ return gates;
+ }
+
+ @Override
+ public Map<String, Declaration> getInnerDeclarations() {
+ return Map.of();
+ }
+
+ @Override
+ public Map<String, Declaration> getOuterDeclarations() {
+ return Map.of();
+ }
+
+ @Override
+ public Declaration resolveDeclaration(String identifier) {
+ return null;
+ }
+
+ @Override
+ public RuleConditionElement clone() {
+ Sequence clone = new Sequence(sequenceIndex);
+ clone.steps = this.steps;
+ clone.gates = this.gates;
+ clone.filters = this.filters;
+ clone.controller = this.controller;
+ return clone;
+ }
+
+ @Override
+ public List<? extends RuleConditionElement> getNestedElements() {
+ return List.of();
+ }
+
+ @Override
+ public boolean isPatternScopeDelimiter() {
+ return false;
+ }
+
+ @Override
+ public void writeExternal(ObjectOutput out) throws IOException {
+ throw new UnsupportedOperationException("Sequence does not support serialization");
+ }
+
+ @Override
+ public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
+ throw new UnsupportedOperationException("Sequence does not support serialization");
+ }
+
+ public void start(SequenceMemory memory, ValueResolver valueResolver) {
+ controller.start(memory, valueResolver);
+ }
+
+ public void next(SequenceMemory sequenceMemory, ValueResolver valueResolver) {
+ controller.next(sequenceMemory, valueResolver);
+ }
+
+ public interface SequenceController {
+ default void start(SequenceMemory sequenceMemory, ValueResolver valueResolver) {
+ sequenceMemory.setStep(0);
+ sequenceMemory.sequence.steps[0].activate(sequenceMemory, valueResolver);
+ }
+
+ default void next(SequenceMemory memory, ValueResolver valueResolver) {
+
+ }
+
+ void end(SequenceMemory memory, ValueResolver valueResolver);
+ }
+
+ public static class DefaultController implements SequenceController {
+ private static final DefaultController INSTANCE = new DefaultController();
+
+ private DefaultController() {}
+
+ public static DefaultController getInstance() {
+ return INSTANCE;
+ }
+
+ public void next(SequenceMemory sequenceMemory, ValueResolver valueResolver) {
+ Sequence sequence = sequenceMemory.getSequence();
+ int step = sequenceMemory.getStep();
+
+ sequence.steps[step].deactivate(sequenceMemory, valueResolver);
+ step = sequenceMemory.incrementStep();
+
+ if (step < sequenceMemory.getSequence().getSteps().length) {
+ sequence.steps[step].activate(sequenceMemory, valueResolver);
+ } else {
+ end(sequenceMemory, valueResolver);
+ }
+ }
+
+ @Override
+ public void end(SequenceMemory sequenceMemory, ValueResolver valueResolver) {
+ sequenceMemory.getSequencerMemory().match(valueResolver);
+ }
+
+ @Override
+ public String toString() {
+ return "DefaultController{}";
+ }
+ }
+
+ public static class SequenceMemory {
+ private final Sequence sequence;
+
+ private int step;
+
+ private final SequencerMemory sequencerMemory;
+
+ private final SignalAdapter[] signalAdapters;
+
+ private final SignalAdapter[] activeSignalAdapters;
+
+ private final long[] gateMemory;
+
+ private final boolean[] gateMatched;
+
+ private CircularArrayList<Object> data;
+
+ public SequenceMemory(SequencerMemory sequencerMemory, Sequence sequence, CircularArrayList<Object> data,
+ SignalAdapter[] signalAdapters, SignalAdapter[] activeSignalAdapters,
+ long[] gateMemory, long[] counterMemories) {
+ this.sequencerMemory = sequencerMemory;
+ this.data = data;
+ this.sequence = sequence;
+ this.signalAdapters = signalAdapters;
+ this.activeSignalAdapters = activeSignalAdapters;
+ this.gateMemory = gateMemory;
+ this.gateMatched = new boolean[gateMemory.length + counterMemories.length];
+ }
+
+ public Sequence getSequence() {
+ return sequence;
+ }
+
+ public SequencerMemory getSequencerMemory() {
+ return sequencerMemory;
+ }
+
+ public boolean isLogicGateMatched(int index) {
+ return gateMatched[index];
+ }
+
+ public void setLogicGateMatched(int index, boolean matched) {
+ gateMatched[index] = matched;
+ }
+
+ public SignalAdapter[] getSignalAdapters() {
+ return signalAdapters;
+ }
+
+ public SignalAdapter[] getActiveSignalAdapters() {
+ return activeSignalAdapters;
+ }
+
+ public long[] getLogicGateMemory() {
+ return gateMemory;
+ }
+
+ public int incrementStep() {
+ return ++step;
+ }
+
+ public int getStep() {
+ return step;
+ }
+
+ public void setStep(int step) {
+ this.step = step;
+ }
+
+ public void setData(int filterIndex, FactHandle handle) {
+ data.put(filterIndex, handle);
+ }
+
+ public SignalAdapter activateSignalAdapter(int filterIndex, LogicGate gate, int signalAdapterIndex, int signalBitIndex) {
+ if (activeSignalAdapters[signalAdapterIndex] != null) {
+ throw new RuntimeException("Defensive coding, this should not be re-entrant");
+ }
+
+ SignalAdapter signalAdapter = signalAdapters[signalAdapterIndex];
+
+ if (signalAdapter == null) {
+ signalAdapter = new SignalAdapter(gate, signalBitIndex, filterIndex, this);
+ signalAdapters[signalAdapterIndex] = signalAdapter;
+ }
+
+ activeSignalAdapters[signalAdapterIndex] = signalAdapter;
+
+ DynamicFilter filter = sequencerMemory.getActiveDynamicFilter(filterIndex);
+ filter.addSignalAdapter(signalAdapter);
+
+ return signalAdapter;
+ }
+
+ public void deactivateSignalAdapter(int filterIndex, int signalAdapterIndex) {
+ SignalAdapter signalAdapter = activeSignalAdapters[signalAdapterIndex];
+ if (signalAdapter == null) {
+ // Already deactivated — no-op to prevent a double-remove from the linked filter list.
+ return;
+ }
+ activeSignalAdapters[signalAdapterIndex] = null;
+
+ DynamicFilter filter = sequencerMemory.getActiveDynamicFilter(filterIndex);
+ filter.removeSignalAdapter(signalAdapter);
+
+ if (filter.getSignalAdapters().isEmpty()) {
+ sequencerMemory.removeActiveFilter(filter);
+ }
+ }
+
+ public void resetLogicGateMemory(int gateIndex, ValueResolver valueResolver) {
+ gateMemory[gateIndex] = 0;
+ gateMatched[gateIndex] = false;
+ }
+
+ @Override
+ public String toString() {
+ return "SequenceMemory{" +
+ "sequence=" + sequence.getSequenceIndex() +
+ ", step=" + step +
+ '}';
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "Sequence{" +
+ "sequenceIndex=" + sequenceIndex +
+ ", steps=" + Arrays.toString(steps) +
+ ", controller=" + controller +
+ '}';
+ }
+}
diff --git a/drools-base/src/main/java/org/drools/base/reteoo/sequencing/Sequencer.java b/drools-base/src/main/java/org/drools/base/reteoo/sequencing/Sequencer.java
new file mode 100644
index 00000000000..e1aeb4420fa
--- /dev/null
+++ b/drools-base/src/main/java/org/drools/base/reteoo/sequencing/Sequencer.java
@@ -0,0 +1,70 @@
+/*
+ * 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.base.reteoo.sequencing;
+
+import org.drools.base.base.ValueResolver;
+import org.drools.base.reteoo.sequencing.Sequence.SequenceMemory;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class Sequencer {
+
+ private final Sequence sequence;
+
+ private final Sequence[] sequences;
+
+ public Sequencer(Sequence sequence) {
+ this.sequence = sequence;
+ this.sequences = populateSequences(sequence, new ArrayList<>()).stream().toArray(Sequence[]::new);
+ }
+
+ public static List<Sequence> populateSequences(Sequence sequence, List<Sequence> list) {
+ list.add(sequence);
+ return list;
+ }
+
+ public Sequence[] getSequences() {
+ return sequences;
+ }
+
+ public void start(SequencerMemory memory, ValueResolver valueResolver) {
+ SequenceMemory sequenceMemory = memory.getOrCreateSequenceMemory(sequence, memory.getData());
+ memory.setChildSequenceMemory(sequenceMemory);
+ sequence.start(sequenceMemory, valueResolver);
+ }
+
+ public void stop(SequenceMemory memory, ValueResolver valueResolver) {
+ if (memory != null) {
+ Sequence sequence = memory.getSequence();
+ int step = memory.getStep();
+ if (step < sequence.getSteps().length) {
+ sequence.getSteps()[step].deactivate(memory, valueResolver);
+ }
+ // Mark terminal so a subsequent stop() (e.g. retract after completion) is a no-op.
+ memory.setStep(sequence.getSteps().length);
+ }
+ }
+
+ public Sequence getSequence() {
+ return sequence;
+ }
+
+
+}
diff --git a/drools-base/src/main/java/org/drools/base/reteoo/sequencing/SequencerMemory.java b/drools-base/src/main/java/org/drools/base/reteoo/sequencing/SequencerMemory.java
new file mode 100644
index 00000000000..aa48e2dbb05
--- /dev/null
+++ b/drools-base/src/main/java/org/drools/base/reteoo/sequencing/SequencerMemory.java
@@ -0,0 +1,47 @@
+/*
+ * 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.base.reteoo.sequencing;
+
+import org.drools.base.base.ValueResolver;
+import org.drools.base.reteoo.BaseTuple;
+import org.drools.base.reteoo.DynamicFilter;
+import org.drools.base.reteoo.sequencing.Sequence.SequenceMemory;
+import org.drools.base.util.CircularArrayList;
+
+public interface SequencerMemory {
+ BaseTuple getLeftTuple();
+
+ CircularArrayList<Object> getData();
+
+ Sequencer getSequencer();
+
+ SequenceMemory getChildSequenceMemory();
+
+ void setChildSequenceMemory(SequenceMemory childSequenceMemory);
+
+ SequenceMemory getOrCreateSequenceMemory(Sequence sequence, CircularArrayList<Object> newData);
+
+ SequenceMemory getSequenceMemory(Sequence sequence);
+
+ void match(ValueResolver valueResolver);
+
+ DynamicFilter getActiveDynamicFilter(int filterIndex);
+
+ void removeActiveFilter(DynamicFilter filter);
+}
diff --git a/drools-model/drools-canonical-model/src/main/java/org/drools/model/view/ExistentialExprViewItem.java b/drools-base/src/main/java/org/drools/base/reteoo/sequencing/signalprocessors/Gates.java
similarity index 53%
copy from drools-model/drools-canonical-model/src/main/java/org/drools/model/view/ExistentialExprViewItem.java
copy to drools-base/src/main/java/org/drools/base/reteoo/sequencing/signalprocessors/Gates.java
index d74396d1070..0a7acdea225 100644
--- a/drools-model/drools-canonical-model/src/main/java/org/drools/model/view/ExistentialExprViewItem.java
+++ b/drools-base/src/main/java/org/drools/base/reteoo/sequencing/signalprocessors/Gates.java
@@ -16,33 +16,11 @@
* specific language governing permissions and limitations
* under the License.
*/
-package org.drools.model.view;
+package org.drools.base.reteoo.sequencing.signalprocessors;
-import org.drools.model.Condition;
-import org.drools.model.Variable;
+public class Gates {
+ public static boolean and(long a, long b) {return a == b;}
-public class ExistentialExprViewItem<T> extends AbstractExprViewItem<T> {
+ public static boolean or(long a, long b) {return (a & b) != 0;}
- private final Condition.Type type;
- private final ViewItem expression;
-
- public ExistentialExprViewItem(Condition.Type type, ViewItem expression) {
- super(expression.getFirstVariable());
- this.type = type;
- this.expression = expression;
- }
-
- public ViewItem getExpression() {
- return expression;
- }
-
- @Override
- public Variable<?>[] getVariables() {
- return expression.getVariables();
- }
-
- @Override
- public Condition.Type getType() {
- return type;
- }
}
diff --git a/drools-model/drools-canonical-model/src/main/java/org/drools/model/view/ExistentialExprViewItem.java b/drools-base/src/main/java/org/drools/base/reteoo/sequencing/signalprocessors/LogicCircuit.java
similarity index 54%
copy from drools-model/drools-canonical-model/src/main/java/org/drools/model/view/ExistentialExprViewItem.java
copy to drools-base/src/main/java/org/drools/base/reteoo/sequencing/signalprocessors/LogicCircuit.java
index d74396d1070..4772da69d6e 100644
--- a/drools-model/drools-canonical-model/src/main/java/org/drools/model/view/ExistentialExprViewItem.java
+++ b/drools-base/src/main/java/org/drools/base/reteoo/sequencing/signalprocessors/LogicCircuit.java
@@ -16,33 +16,22 @@
* specific language governing permissions and limitations
* under the License.
*/
-package org.drools.model.view;
+package org.drools.base.reteoo.sequencing.signalprocessors;
-import org.drools.model.Condition;
-import org.drools.model.Variable;
+public class LogicCircuit {
-public class ExistentialExprViewItem<T> extends AbstractExprViewItem<T> {
+ private final LogicGate[] gates;
- private final Condition.Type type;
- private final ViewItem expression;
-
- public ExistentialExprViewItem(Condition.Type type, ViewItem expression) {
- super(expression.getFirstVariable());
- this.type = type;
- this.expression = expression;
+ public LogicCircuit(LogicGate... gates) {
+ this.gates = gates;
}
- public ViewItem getExpression() {
- return expression;
+ public LogicGate[] getGates() {
+ return gates;
}
- @Override
- public Variable<?>[] getVariables() {
- return expression.getVariables();
+ public interface LongBiPredicate {
+ boolean test(long a, long b);
}
- @Override
- public Condition.Type getType() {
- return type;
- }
}
diff --git a/drools-base/src/main/java/org/drools/base/reteoo/sequencing/signalprocessors/LogicGate.java b/drools-base/src/main/java/org/drools/base/reteoo/sequencing/signalprocessors/LogicGate.java
new file mode 100644
index 00000000000..a3e94f71a36
--- /dev/null
+++ b/drools-base/src/main/java/org/drools/base/reteoo/sequencing/signalprocessors/LogicGate.java
@@ -0,0 +1,123 @@
+/*
+ * 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.base.reteoo.sequencing.signalprocessors;
+
+import org.drools.base.base.ValueResolver;
+import org.drools.base.reteoo.sequencing.signalprocessors.LogicCircuit.LongBiPredicate;
+import org.drools.base.reteoo.sequencing.Sequence.SequenceMemory;
+
+public class LogicGate extends SignalProcessor {
+ protected long allMatched;
+
+ private SignalProcessor output;
+
+ private final LongBiPredicate predicate;
+
+ private final int gateIndex;
+
+ private LogicGate[] inputGates = EMPTY_INPUT_GATES;
+
+ private final int[] filterIndexes;
+
+ private int[] signalAdapterIndexes;
+
+ private static final LogicGate[] EMPTY_INPUT_GATES = new LogicGate[0];
+
+ public LogicGate(LongBiPredicate predicate, int gateIndex, int[] filterIndexes, int[] signalAdapterIndexes, int nbrOfInputGates) {
+ this.predicate = predicate;
+
+ this.filterIndexes = filterIndexes;
+ this.signalAdapterIndexes = signalAdapterIndexes;
+
+ int totalInputs = signalAdapterIndexes.length + nbrOfInputGates;
+ if (totalInputs > Long.SIZE) {
+ throw new IllegalArgumentException(
+ "LogicGate supports at most " + Long.SIZE + " inputs; got " + totalInputs);
+ }
+ for (int i = 0; i < totalInputs; i++) {
+ allMatched = allMatched | (1L << i);
+ }
+
+ this.gateIndex = gateIndex;
+ }
+
+ public int[] getSignalAdapterIndexes() {
+ return signalAdapterIndexes;
+ }
+
+ public void setInputGates(LogicGate... inputGates) {
+ this.inputGates = inputGates;
+ }
+
+ public void setOutput(SignalProcessor output) {
+ this.output = output;
+ }
+
+ @Override
+ public void consume(SequenceMemory memory, ValueResolver valueResolver) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void consume(int signalBitIndex, SequenceMemory memory, ValueResolver valueResolver) {
+ long currentMatched = memory.getLogicGateMemory()[gateIndex] | (1L << (signalBitIndex - 1));
+ memory.getLogicGateMemory()[gateIndex] = currentMatched;
+
+ boolean matched = predicate.test(currentMatched, allMatched);
+
+ if (matched && !memory.isLogicGateMatched(gateIndex)) {
+ memory.setLogicGateMatched(gateIndex, true);
+ propagate(memory, valueResolver);
+ }
+ }
+
+ public void propagate(SequenceMemory memory, ValueResolver valueResolver) {
+ resetPrior(memory, valueResolver);
+ output.consume(memory, valueResolver);
+ }
+
+ public void resetPrior(SequenceMemory memory, ValueResolver valueResolver) {
+ for (LogicGate gate : inputGates) {
+ gate.reset(memory, valueResolver);
+ }
+
+ memory.resetLogicGateMemory(gateIndex, valueResolver);
+ }
+
+ public void reset(SequenceMemory memory, ValueResolver valueResolver) {
+ resetPrior(memory, valueResolver);
+ output.reset(memory, valueResolver);
+ }
+
+ public void activate(SequenceMemory memory) {
+ for (int i = 0; i < filterIndexes.length; i++) {
+ memory.activateSignalAdapter(filterIndexes[i], this, signalAdapterIndexes[i], i + 1); // bit indexes start at 1
+ }
+
+ }
+
+ public void deactivate(SequenceMemory memory, ValueResolver valueResolver) {
+ for (int i = 0; i < filterIndexes.length; i++) {
+ memory.deactivateSignalAdapter(filterIndexes[i], signalAdapterIndexes[i]);
+ }
+
+ memory.resetLogicGateMemory(gateIndex, valueResolver);
+ }
+
+}
diff --git a/drools-base/src/main/java/org/drools/base/reteoo/sequencing/signalprocessors/LogicGateOutputSignalProcessor.java b/drools-base/src/main/java/org/drools/base/reteoo/sequencing/signalprocessors/LogicGateOutputSignalProcessor.java
new file mode 100644
index 00000000000..c7546b4a8ab
--- /dev/null
+++ b/drools-base/src/main/java/org/drools/base/reteoo/sequencing/signalprocessors/LogicGateOutputSignalProcessor.java
@@ -0,0 +1,49 @@
+/*
+ * 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.base.reteoo.sequencing.signalprocessors;
+
+import org.drools.base.base.ValueResolver;
+import org.drools.base.reteoo.sequencing.Sequence.SequenceMemory;
+
+public class LogicGateOutputSignalProcessor extends SignalProcessor {
+ private final LogicGate gate;
+ private final int index;
+
+ public LogicGateOutputSignalProcessor(SignalIndex signalIndex) {
+ this.gate = signalIndex.getGate();
+ this.index = signalIndex.getBitIndex();
+ }
+
+ @Override
+ public void consume(SequenceMemory memory, ValueResolver valueResolver) {
+ gate.consume(index, memory, valueResolver);
+ }
+
+ @Override
+ public void consume(int signalBitIndex, SequenceMemory memory, ValueResolver valueResolver) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ protected void reset(SequenceMemory memory, ValueResolver valueResolver) {
+ // No-op: LogicGateOutputSignalProcessor is a fan-out node whose reset is driven
+ // by each downstream LogicGate resetting itself via resetPrior(). There is no
+ // state held here that needs clearing.
+ }
+}
diff --git a/drools-model/drools-canonical-model/src/main/java/org/drools/model/view/ExistentialExprViewItem.java b/drools-base/src/main/java/org/drools/base/reteoo/sequencing/signalprocessors/SignalIndex.java
similarity index 55%
copy from drools-model/drools-canonical-model/src/main/java/org/drools/model/view/ExistentialExprViewItem.java
copy to drools-base/src/main/java/org/drools/base/reteoo/sequencing/signalprocessors/SignalIndex.java
index d74396d1070..e92efd30328 100644
--- a/drools-model/drools-canonical-model/src/main/java/org/drools/model/view/ExistentialExprViewItem.java
+++ b/drools-base/src/main/java/org/drools/base/reteoo/sequencing/signalprocessors/SignalIndex.java
@@ -16,33 +16,26 @@
* specific language governing permissions and limitations
* under the License.
*/
-package org.drools.model.view;
+package org.drools.base.reteoo.sequencing.signalprocessors;
-import org.drools.model.Condition;
-import org.drools.model.Variable;
+public class SignalIndex {
+ private final LogicGate gate;
+ private final int bitIndex;
-public class ExistentialExprViewItem<T> extends AbstractExprViewItem<T> {
-
- private final Condition.Type type;
- private final ViewItem expression;
-
- public ExistentialExprViewItem(Condition.Type type, ViewItem expression) {
- super(expression.getFirstVariable());
- this.type = type;
- this.expression = expression;
+ public static SignalIndex of(LogicGate gate, int bitIndex) {
+ return new SignalIndex(gate, bitIndex);
}
- public ViewItem getExpression() {
- return expression;
+ public SignalIndex(LogicGate gate, int bitIndex) {
+ this.gate = gate;
+ this.bitIndex = bitIndex;
}
- @Override
- public Variable<?>[] getVariables() {
- return expression.getVariables();
+ public LogicGate getGate() {
+ return gate;
}
- @Override
- public Condition.Type getType() {
- return type;
+ public int getBitIndex() {
+ return bitIndex;
}
}
diff --git a/drools-model/drools-canonical-model/src/main/java/org/drools/model/view/ExistentialExprViewItem.java b/drools-base/src/main/java/org/drools/base/reteoo/sequencing/signalprocessors/SignalProcessor.java
similarity index 53%
copy from drools-model/drools-canonical-model/src/main/java/org/drools/model/view/ExistentialExprViewItem.java
copy to drools-base/src/main/java/org/drools/base/reteoo/sequencing/signalprocessors/SignalProcessor.java
index d74396d1070..b0f3c32d78b 100644
--- a/drools-model/drools-canonical-model/src/main/java/org/drools/model/view/ExistentialExprViewItem.java
+++ b/drools-base/src/main/java/org/drools/base/reteoo/sequencing/signalprocessors/SignalProcessor.java
@@ -16,33 +16,16 @@
* specific language governing permissions and limitations
* under the License.
*/
-package org.drools.model.view;
+package org.drools.base.reteoo.sequencing.signalprocessors;
-import org.drools.model.Condition;
-import org.drools.model.Variable;
+import org.drools.base.base.ValueResolver;
+import org.drools.base.reteoo.sequencing.Sequence.SequenceMemory;
-public class ExistentialExprViewItem<T> extends AbstractExprViewItem<T> {
+public abstract class SignalProcessor {
- private final Condition.Type type;
- private final ViewItem expression;
+ public abstract void consume(SequenceMemory memory, ValueResolver valueResolver);
- public ExistentialExprViewItem(Condition.Type type, ViewItem expression) {
- super(expression.getFirstVariable());
- this.type = type;
- this.expression = expression;
- }
+ public abstract void consume(int signalBitIndex, SequenceMemory memory, ValueResolver valueResolver);
- public ViewItem getExpression() {
- return expression;
- }
-
- @Override
- public Variable<?>[] getVariables() {
- return expression.getVariables();
- }
-
- @Override
- public Condition.Type getType() {
- return type;
- }
+ protected abstract void reset(SequenceMemory memory, ValueResolver valueResolver);
}
diff --git a/drools-model/drools-canonical-model/src/main/java/org/drools/model/Condition.java b/drools-base/src/main/java/org/drools/base/reteoo/sequencing/signalprocessors/TerminatingSignalProcessor.java
similarity index 50%
copy from drools-model/drools-canonical-model/src/main/java/org/drools/model/Condition.java
copy to drools-base/src/main/java/org/drools/base/reteoo/sequencing/signalprocessors/TerminatingSignalProcessor.java
index 6897ad2478f..f3477a5a2ee 100644
--- a/drools-model/drools-canonical-model/src/main/java/org/drools/model/Condition.java
+++ b/drools-base/src/main/java/org/drools/base/reteoo/sequencing/signalprocessors/TerminatingSignalProcessor.java
@@ -16,43 +16,33 @@
* specific language governing permissions and limitations
* under the License.
*/
-package org.drools.model;
+package org.drools.base.reteoo.sequencing.signalprocessors;
-import java.util.Collections;
-import java.util.List;
+import org.drools.base.base.ValueResolver;
+import org.drools.base.reteoo.sequencing.Sequence.SequenceMemory;
-public interface Condition {
+public class TerminatingSignalProcessor extends SignalProcessor {
+ private static final TerminatingSignalProcessor MATCH = new TerminatingSignalProcessor();
- default List<Condition> getSubConditions() {
- return Collections.emptyList();
+ private TerminatingSignalProcessor() {
}
- Type getType();
+ public static TerminatingSignalProcessor get() {
+ return MATCH;
+ }
- Variable<?>[] getBoundVariables();
+ @Override
+ public void consume(SequenceMemory memory, ValueResolver valueResolver) {
+ memory.getSequence().next(memory, valueResolver);
+ }
- default Condition cloneCondition() {
+ @Override
+ public void consume(int signalBitIndex, SequenceMemory memory, ValueResolver valueResolver) {
throw new UnsupportedOperationException();
}
- enum Type {
- PATTERN( false ), EVAL( false ), QUERY( false ),
- ACCUMULATE( false ), GROUP_BY( false ), TEMPORAL( false ),
- RECEIVER( false ), SENDER( false ),
- OR( true ), AND( true ), NOT( false ), EXISTS( false ), FORALL( false ), CONSEQUENCE( false );
-
- private final boolean composite;
-
- Type( boolean composite ) {
- this.composite = composite;
- }
-
- public boolean isComposite() {
- return composite;
- }
-
- public boolean createsScope() {
- return this == OR;
- }
+ @Override
+ protected void reset(SequenceMemory memory, ValueResolver valueResolver) {
+ throw new UnsupportedOperationException();
}
}
diff --git a/drools-model/drools-canonical-model/src/main/java/org/drools/model/view/ExistentialExprViewItem.java b/drools-base/src/main/java/org/drools/base/reteoo/sequencing/steps/AbstractStep.java
similarity index 54%
copy from drools-model/drools-canonical-model/src/main/java/org/drools/model/view/ExistentialExprViewItem.java
copy to drools-base/src/main/java/org/drools/base/reteoo/sequencing/steps/AbstractStep.java
index d74396d1070..4634421f62b 100644
--- a/drools-model/drools-canonical-model/src/main/java/org/drools/model/view/ExistentialExprViewItem.java
+++ b/drools-base/src/main/java/org/drools/base/reteoo/sequencing/steps/AbstractStep.java
@@ -16,33 +16,15 @@
* specific language governing permissions and limitations
* under the License.
*/
-package org.drools.model.view;
+package org.drools.base.reteoo.sequencing.steps;
-import org.drools.model.Condition;
-import org.drools.model.Variable;
+import org.drools.base.reteoo.sequencing.Sequence;
-public class ExistentialExprViewItem<T> extends AbstractExprViewItem<T> {
+public abstract class AbstractStep implements Step {
- private final Condition.Type type;
- private final ViewItem expression;
+ protected final Sequence sequence; // the sequence the step is in
- public ExistentialExprViewItem(Condition.Type type, ViewItem expression) {
- super(expression.getFirstVariable());
- this.type = type;
- this.expression = expression;
- }
-
- public ViewItem getExpression() {
- return expression;
- }
-
- @Override
- public Variable<?>[] getVariables() {
- return expression.getVariables();
- }
-
- @Override
- public Condition.Type getType() {
- return type;
+ public AbstractStep(Sequence sequence) {
+ this.sequence = sequence;
}
}
diff --git a/drools-base/src/main/java/org/drools/base/reteoo/sequencing/steps/LogicCircuitStep.java b/drools-base/src/main/java/org/drools/base/reteoo/sequencing/steps/LogicCircuitStep.java
new file mode 100644
index 00000000000..07010de2370
--- /dev/null
+++ b/drools-base/src/main/java/org/drools/base/reteoo/sequencing/steps/LogicCircuitStep.java
@@ -0,0 +1,50 @@
+/*
+ * 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.base.reteoo.sequencing.steps;
+
+import org.drools.base.base.ValueResolver;
+import org.drools.base.reteoo.sequencing.signalprocessors.LogicCircuit;
+import org.drools.base.reteoo.sequencing.signalprocessors.LogicGate;
+import org.drools.base.reteoo.sequencing.Sequence;
+import org.drools.base.reteoo.sequencing.Sequence.SequenceMemory;
+
+public class LogicCircuitStep extends AbstractStep implements Step {
+ private final LogicCircuit circuit;
+
+ public LogicCircuitStep(Sequence sequence, LogicCircuit circuit) {
+ super(sequence);
+ this.circuit = circuit;
+ }
+
+ public LogicCircuit getCircuit() {
+ return circuit;
+ }
+
+ public void activate(SequenceMemory sequenceMemory, ValueResolver valueResolver) {
+ for (LogicGate gate : circuit.getGates()) {
+ gate.activate(sequenceMemory);
+ }
+ }
+
+ public void deactivate(SequenceMemory sequenceMemory, ValueResolver valueResolver) {
+ for (LogicGate gate : circuit.getGates()) {
+ gate.deactivate(sequenceMemory, valueResolver);
+ }
+ }
+}
diff --git a/drools-base/src/main/java/org/drools/base/reteoo/sequencing/steps/Step.java b/drools-base/src/main/java/org/drools/base/reteoo/sequencing/steps/Step.java
new file mode 100644
index 00000000000..b2348fefb05
--- /dev/null
+++ b/drools-base/src/main/java/org/drools/base/reteoo/sequencing/steps/Step.java
@@ -0,0 +1,49 @@
+/*
+ * 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.base.reteoo.sequencing.steps;
+
+import org.drools.base.base.ValueResolver;
+import org.drools.base.reteoo.sequencing.signalprocessors.LogicCircuit;
+import org.drools.base.reteoo.sequencing.Sequence;
+import org.drools.base.reteoo.sequencing.Sequence.SequenceMemory;
+
+public interface Step {
+ void activate(SequenceMemory memory, ValueResolver valueResolver);
+
+ void deactivate(SequenceMemory memory, ValueResolver valueResolver);
+
+ static StepFactory of(LogicCircuit circuit) {
+ StepFactory factory = new StepFactory();
+ factory.setCircuit(circuit);
+ return factory;
+ }
+
+ class StepFactory {
+ private LogicCircuit circuit;
+
+ public void setCircuit(LogicCircuit circuit) {
+ this.circuit = circuit;
+ }
+
+ public Step createStep(Sequence sequence) {
+ return new LogicCircuitStep(sequence, circuit);
+ }
+ }
+
+}
diff --git a/drools-base/src/main/java/org/drools/base/rule/constraint/CombinedAlphaConstraint.java b/drools-base/src/main/java/org/drools/base/rule/constraint/CombinedAlphaConstraint.java
new file mode 100644
index 00000000000..5344678ea3b
--- /dev/null
+++ b/drools-base/src/main/java/org/drools/base/rule/constraint/CombinedAlphaConstraint.java
@@ -0,0 +1,110 @@
+/*
+ * 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.base.rule.constraint;
+
+import java.io.IOException;
+import java.io.ObjectInput;
+import java.io.ObjectOutput;
+import java.util.Arrays;
+
+import org.drools.base.base.ValueResolver;
+import org.drools.base.rule.Declaration;
+import org.kie.api.runtime.rule.FactHandle;
+
+/**
+ * ANDs two or more {@link AlphaNodeFieldConstraint}s into a single constraint.
+ * Used by the sequencing runtime when a sequence step carries multiple expressions:
+ * every expression must be satisfied for the step to match.
+ */
+public class CombinedAlphaConstraint implements AlphaNodeFieldConstraint {
+
+ private AlphaNodeFieldConstraint[] constraints;
+
+ public CombinedAlphaConstraint(AlphaNodeFieldConstraint[] constraints) {
+ this.constraints = constraints;
+ }
+
+ @Override
+ public boolean isAllowed(FactHandle handle, ValueResolver valueResolver) {
+ for (AlphaNodeFieldConstraint c : constraints) {
+ if (!c.isAllowed(handle, valueResolver)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ @Override
+ public Declaration[] getRequiredDeclarations() {
+ return new Declaration[0];
+ }
+
+ @Override
+ public void replaceDeclaration(Declaration oldDecl, Declaration newDecl) {
+ // Sequence step constraints are always self-contained alpha constraints —
+ // they carry no cross-pattern declaration references, so getRequiredDeclarations()
+ // returns empty and LogicTransformer never calls this method.
+ }
+
+ @Override
+ public CombinedAlphaConstraint clone() {
+ AlphaNodeFieldConstraint[] cloned = new AlphaNodeFieldConstraint[constraints.length];
+ for (int i = 0; i < constraints.length; i++) {
+ cloned[i] = (AlphaNodeFieldConstraint) constraints[i].clone();
+ }
+ return new CombinedAlphaConstraint(cloned);
+ }
+
+ @Override
+ public AlphaNodeFieldConstraint cloneIfInUse() {
+ return clone();
+ }
+
+ @Override
+ public ConstraintType getType() {
+ return ConstraintType.ALPHA;
+ }
+
+ @Override
+ public boolean isTemporal() {
+ return false;
+ }
+
+ @Override
+ public void writeExternal(ObjectOutput out) throws IOException {
+ out.writeObject(constraints);
+ }
+
+ @Override
+ public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
+ constraints = (AlphaNodeFieldConstraint[]) in.readObject();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof CombinedAlphaConstraint)) return false;
+ return Arrays.equals(constraints, ((CombinedAlphaConstraint) o).constraints);
+ }
+
+ @Override
+ public int hashCode() {
+ return Arrays.hashCode(constraints);
+ }
+}
diff --git a/drools-base/src/main/java/org/drools/base/util/CircularArrayList.java b/drools-base/src/main/java/org/drools/base/util/CircularArrayList.java
new file mode 100644
index 00000000000..0739de1fc21
--- /dev/null
+++ b/drools-base/src/main/java/org/drools/base/util/CircularArrayList.java
@@ -0,0 +1,128 @@
+/*
+ * 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.base.util;
+
+import java.lang.reflect.Array;
+import java.util.Arrays;
+
+public class CircularArrayList<E> {
+ private E[] array;
+ private int head = 0;
+ // windowSize is the logical max-kept capacity (as requested by the caller).
+ // arrayCapacity is rounded up to the nearest power of two for fast modulo via mask.
+ private final int windowSize;
+ private final int arrayCapacity;
+ private final int mask;
+ private Class cls;
+
+ public CircularArrayList(int capacity) {
+ this(Object.class, capacity);
+ }
+
+ public CircularArrayList(Class cls, int capacity) {
+ // Round up to the nearest power of two (same strategy as HashMap).
+ int cap = capacity <= 1 ? 1 : Integer.highestOneBit(capacity - 1) << 1;
+ this.windowSize = capacity;
+ this.arrayCapacity = cap;
+ this.mask = cap - 1;
+ this.array = (E[]) new Object[cap];
+ this.cls = cls;
+ }
+
+ public boolean set(int index, E e) {
+ array[index & mask] = e;
+ return true;
+ }
+
+ /**
+ * Write {@code e} at the given logical {@code index} and advance {@code head}
+ * to {@code index + 1} if it has not reached that point yet. This allows
+ * writing to an arbitrary slot (e.g. by filterIndex) while keeping the
+ * {@code get()} range check valid.
+ */
+ public void put(int index, E e) {
+ array[index & mask] = e;
+ if (index >= head) {
+ head = index + 1;
+ }
+ }
+
+ public boolean add(E e) {
+ array[head++ & mask] = e;
+ return true;
+ }
+
+ public E getHead() {
+ return array[(head-1) & mask];
+ }
+
+ public E getHeadMinus(int i) {
+ return array[(head-1-i) & mask];
+ }
+
+ public E get(int index) {
+ if (index < 0 || index < head - windowSize || index >= head) {
+ throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + windowSize);
+ }
+
+ return array[index & mask];
+ }
+
+ public int size() {
+ return head;
+ }
+
+ public boolean isEmpty() {
+ return size() == 0;
+ }
+
+ public E[] toArray() {
+ int headIndex = head & mask;
+ E[] trg;
+ if (head <= windowSize) {
+ // Buffer has not yet wrapped around the logical window.
+ trg = (E[]) Array.newInstance(cls, head);
+ System.arraycopy(array, 0, trg, 0, head);
+ } else {
+ // Buffer is full: oldest entry starts at headIndex (mod arrayCapacity).
+ trg = (E[]) Array.newInstance(cls, windowSize);
+ // headIndex is where the *next* write goes, which is also the oldest slot.
+ // We need to copy windowSize elements starting from that position.
+ int oldestIndex = (head - windowSize) & mask;
+ if (oldestIndex + windowSize <= arrayCapacity) {
+ System.arraycopy(array, oldestIndex, trg, 0, windowSize);
+ } else {
+ int firstPart = arrayCapacity - oldestIndex;
+ System.arraycopy(array, oldestIndex, trg, 0, firstPart);
+ System.arraycopy(array, 0, trg, firstPart, windowSize - firstPart);
+ }
+ }
+
+ return trg;
+ }
+
+ @Override
+ public String toString() {
+ return "CircularArrayList{" +
+ "array=" + Arrays.toString(array) +
+ ", head=" + head +
+ ", windowSize=" + windowSize +
+ '}';
+ }
+}
diff --git a/drools-core/src/test/java/org/drools/core/util/LinkedListTest.java b/drools-base/src/test/java/org/drools/base/util/LinkedListTest.java
similarity index 97%
rename from drools-core/src/test/java/org/drools/core/util/LinkedListTest.java
rename to drools-base/src/test/java/org/drools/base/util/LinkedListTest.java
index 28a5f26318b..746993ec2a7 100644
--- a/drools-core/src/test/java/org/drools/core/util/LinkedListTest.java
+++ b/drools-base/src/test/java/org/drools/base/util/LinkedListTest.java
@@ -16,13 +16,10 @@
* specific language governing permissions and limitations
* under the License.
*/
-package org.drools.core.util;
+package org.drools.base.util;
-import org.drools.base.util.AbstractLinkedListNode;
-import org.drools.base.util.DoubleLinkedEntry;
-import org.drools.base.util.LinkedList;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
+import org.junit.Before;
+import org.junit.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.fail;
@@ -35,7 +32,7 @@ public class LinkedListTest {
DoubleLinkedEntry node2 = null;
DoubleLinkedEntry node3 = null;
- @BeforeEach
+ @Before
public void setUp() throws Exception {
this.list = new LinkedList();
this.node1 = new AbstractBaseLinkedListNodeMock();
diff --git a/drools-core/src/main/java/org/drools/core/common/SuperCacheFixer.java b/drools-core/src/main/java/org/drools/core/common/SuperCacheFixer.java
index 2ea4040660c..a9208e092e8 100644
--- a/drools-core/src/main/java/org/drools/core/common/SuperCacheFixer.java
+++ b/drools-core/src/main/java/org/drools/core/common/SuperCacheFixer.java
@@ -42,6 +42,7 @@ import org.drools.core.reteoo.ObjectTypeNodeId;
import org.drools.core.reteoo.QueryElementNode;
import org.drools.core.reteoo.QueryTerminalNode;
import org.drools.core.reteoo.ReactiveFromNode;
+import org.drools.core.reteoo.SequenceNode;
import org.drools.core.reteoo.TupleToObjectNode;
import org.drools.core.reteoo.RightTuple;
import org.drools.core.reteoo.RightTupleSink;
@@ -77,6 +78,7 @@ public class SuperCacheFixer {
case NodeTypeEnums.QueryElementNode: return (QueryElementNode) s;
case NodeTypeEnums.TimerConditionNode: return (TimerNode) s;
case NodeTypeEnums.MockBetaNode: return (LeftTupleNode) s;
+ case NodeTypeEnums.SequenceNode: return (SequenceNode) s;
default:
throw new UnsupportedOperationException("Cannot be cast to LeftTupleNode: " + s);
}
@@ -115,6 +117,7 @@ public class SuperCacheFixer {
case NodeTypeEnums.QueryElementNode: return (QueryElementNode) n;
case NodeTypeEnums.TimerConditionNode: return (TimerNode) n;
case NodeTypeEnums.MockBetaNode: return (LeftTupleSinkNode) n;
+ case NodeTypeEnums.SequenceNode: return (SequenceNode) n;
default:
throw new UnsupportedOperationException("Cannot be cast to LeftTupleNode: " + n);
}
@@ -139,6 +142,7 @@ public class SuperCacheFixer {
case NodeTypeEnums.QueryElementNode: return ((QueryElementNode) s).getInputOtnId();
case NodeTypeEnums.TimerConditionNode: return ((TimerNode) s).getInputOtnId();
case NodeTypeEnums.MockBetaNode: return ((LeftTupleSource)s).getInputOtnId();
+ case NodeTypeEnums.SequenceNode: return ((SequenceNode)s).getInputOtnId();
default:
throw new UnsupportedOperationException("Node does not have an LeftInputOtnId: " + s);
}
@@ -175,6 +179,7 @@ public class SuperCacheFixer {
case NodeTypeEnums.ConditionalBranchNode: return ((ConditionalBranchNode) s).getLeftTupleSource();
case NodeTypeEnums.QueryElementNode: return ((QueryElementNode) s).getLeftTupleSource();
case NodeTypeEnums.TimerConditionNode: return ((TimerNode) s).getLeftTupleSource();
+ case NodeTypeEnums.SequenceNode: return ((SequenceNode)s).getLeftTupleSource();
default:
throw new UnsupportedOperationException("Does not have a LeftTupleSource: " + s);
}
diff --git a/drools-core/src/main/java/org/drools/core/impl/WorkingMemoryReteExpireAction.java b/drools-core/src/main/java/org/drools/core/impl/WorkingMemoryReteExpireAction.java
index 5841d09dfdc..db424cbd2e0 100644
--- a/drools-core/src/main/java/org/drools/core/impl/WorkingMemoryReteExpireAction.java
+++ b/drools-core/src/main/java/org/drools/core/impl/WorkingMemoryReteExpireAction.java
@@ -23,14 +23,14 @@ import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
+import org.drools.base.phreak.PropagationEntry;
+import org.drools.base.phreak.actions.AbstractPropagationEntry;
import org.drools.core.common.DefaultEventHandle;
import org.drools.core.common.DefaultFactHandle;
import org.drools.core.common.PropagationContext;
import org.drools.core.common.ReteEvaluator;
import org.drools.core.common.WorkingMemoryAction;
import org.drools.core.marshalling.MarshallerReaderContext;
-import org.drools.base.phreak.PropagationEntry;
-import org.drools.base.phreak.actions.AbstractPropagationEntry;
import org.drools.core.phreak.actions.AbstractPartitionedPropagationEntry;
import org.drools.core.reteoo.ObjectTypeNode;
import org.drools.core.reteoo.RightTuple;
@@ -110,7 +110,7 @@ public class WorkingMemoryReteExpireAction
}
@Override
- public PropagationEntry getSplitForPartition( int partitionNr ) {
+ public PropagationEntry<ReteEvaluator> getSplitForPartition( int partitionNr ) {
return new PartitionAwareWorkingMemoryReteExpireAction( factHandle, node, partitionNr );
}
diff --git a/drools-core/src/main/java/org/drools/core/phreak/BuildtimeSegmentUtilities.java b/drools-core/src/main/java/org/drools/core/phreak/BuildtimeSegmentUtilities.java
index 7d06a644862..90322386dbc 100644
--- a/drools-core/src/main/java/org/drools/core/phreak/BuildtimeSegmentUtilities.java
+++ b/drools-core/src/main/java/org/drools/core/phreak/BuildtimeSegmentUtilities.java
@@ -40,6 +40,8 @@ import org.drools.core.reteoo.PathEndNode;
import org.drools.core.reteoo.PathEndNode.PathMemSpec;
import org.drools.core.reteoo.QueryElementNode;
import org.drools.core.reteoo.ReactiveFromNode;
+import org.drools.core.reteoo.SegmentMemory.SequenceMemoryPrototype;
+import org.drools.core.reteoo.SequenceNode;
import org.drools.core.reteoo.TupleToObjectNode;
import org.drools.core.reteoo.SegmentMemory.AccumulateMemoryPrototype;
import org.drools.core.reteoo.SegmentMemory.AsyncReceiveMemoryPrototype;
@@ -240,6 +242,9 @@ public class BuildtimeSegmentUtilities {
case NodeTypeEnums.TimerConditionNode:
processTimerNode((TimerNode) node, memories, nodes, nodePosMask);
break;
+ case NodeTypeEnums.SequenceNode:
+ processSequenceNode((SequenceNode) node, memories, nodes, nodePosMask);
+ break;
case NodeTypeEnums.AsyncSendNode:
processAsyncSendNode((AsyncSendNode) node, memories, nodes);
break;
@@ -379,6 +384,12 @@ public class BuildtimeSegmentUtilities {
nodes.add(tupleSource);
}
+ private static void processSequenceNode(SequenceNode tupleSource, List<MemoryPrototype> memories, List<LeftTupleNode> nodes, long nodePosMask) {
+ SequenceMemoryPrototype seqMem = new SequenceMemoryPrototype(nodePosMask);
+ memories.add(seqMem);
+ nodes.add(tupleSource);
+ }
+
private static long processLiaNode(LeftInputAdapterNode tupleSource,
List<MemoryPrototype> memories,
List<LeftTupleNode> nodes,
diff --git a/drools-core/src/main/java/org/drools/core/phreak/PhreakNetworkNodeFactory.java b/drools-core/src/main/java/org/drools/core/phreak/PhreakNetworkNodeFactory.java
index 44413871198..6933b15e092 100644
--- a/drools-core/src/main/java/org/drools/core/phreak/PhreakNetworkNodeFactory.java
+++ b/drools-core/src/main/java/org/drools/core/phreak/PhreakNetworkNodeFactory.java
@@ -19,6 +19,7 @@
package org.drools.core.phreak;
import org.drools.core.common.ReteEvaluator;
+import org.drools.core.reteoo.SequenceNode.PhreakSequenceNode;
import org.kie.api.internal.utils.KieService;
public interface PhreakNetworkNodeFactory extends KieService {
@@ -47,6 +48,8 @@ public interface PhreakNetworkNodeFactory extends KieService {
PhreakAsyncSendNode createPhreakAsyncSendNode(ReteEvaluator reteEvaluator);
+ PhreakSequenceNode createPhreakSequenceNode(ReteEvaluator reteEvaluator);
+
PhreakAsyncReceiveNode createPhreakAsyncReceiveNode(ReteEvaluator reteEvaluator);
PhreakRuleTerminalNode createPhreakRuleTerminalNode(ReteEvaluator reteEvaluator);
diff --git a/drools-core/src/main/java/org/drools/core/phreak/PhreakNetworkNodeFactoryImpl.java b/drools-core/src/main/java/org/drools/core/phreak/PhreakNetworkNodeFactoryImpl.java
index 0c5f117db52..f7a3c56ef49 100644
--- a/drools-core/src/main/java/org/drools/core/phreak/PhreakNetworkNodeFactoryImpl.java
+++ b/drools-core/src/main/java/org/drools/core/phreak/PhreakNetworkNodeFactoryImpl.java
@@ -18,6 +18,8 @@
*/
package org.drools.core.phreak;
+import org.drools.core.reteoo.SequenceNode.PhreakSequenceNode;
+
import org.drools.core.common.ReteEvaluator;
public class PhreakNetworkNodeFactoryImpl implements PhreakNetworkNodeFactory {
@@ -77,6 +79,11 @@ public class PhreakNetworkNodeFactoryImpl implements PhreakNetworkNodeFactory {
return new PhreakTimerNode(reteEvaluator);
}
+ @Override
+ public PhreakSequenceNode createPhreakSequenceNode(ReteEvaluator reteEvaluator) {
+ return new PhreakSequenceNode(reteEvaluator);
+ }
+
@Override
public PhreakAsyncSendNode createPhreakAsyncSendNode(ReteEvaluator reteEvaluator) {
return new PhreakAsyncSendNode(reteEvaluator);
diff --git a/drools-core/src/main/java/org/drools/core/phreak/PhreakTimerNode.java b/drools-core/src/main/java/org/drools/core/phreak/PhreakTimerNode.java
index 481c136a1d5..a520e6b65fb 100644
--- a/drools-core/src/main/java/org/drools/core/phreak/PhreakTimerNode.java
+++ b/drools-core/src/main/java/org/drools/core/phreak/PhreakTimerNode.java
@@ -41,7 +41,6 @@ import org.drools.core.reteoo.TupleFactory;
import org.drools.core.reteoo.TupleImpl;
import org.drools.core.time.Job;
import org.drools.core.time.JobContext;
-import org.drools.base.phreak.PropagationEntry;
import org.drools.base.phreak.actions.AbstractPropagationEntry;
import org.drools.core.time.TimerService;
import org.drools.core.time.impl.DefaultJobHandle;
diff --git a/drools-core/src/main/java/org/drools/core/phreak/RuleNetworkEvaluatorImpl.java b/drools-core/src/main/java/org/drools/core/phreak/RuleNetworkEvaluatorImpl.java
index c6a4759f3b8..9e6c2b6eb32 100644
--- a/drools-core/src/main/java/org/drools/core/phreak/RuleNetworkEvaluatorImpl.java
+++ b/drools-core/src/main/java/org/drools/core/phreak/RuleNetworkEvaluatorImpl.java
@@ -26,6 +26,7 @@ import java.util.List;
import org.drools.base.common.NetworkNode;
import org.drools.base.reteoo.NodeTypeEnums;
+import org.drools.base.util.LinkedList;
import org.drools.core.common.ActivationsManager;
import org.drools.core.common.Memory;
import org.drools.core.common.NodeMemories;
@@ -63,6 +64,9 @@ import org.drools.core.reteoo.QueryTerminalNode;
import org.drools.core.reteoo.ReactiveFromNode;
import org.drools.core.reteoo.RightInputAdapterNode;
import org.drools.core.reteoo.SegmentMemory;
+import org.drools.core.reteoo.SequenceNode;
+import org.drools.core.reteoo.SequenceNode.PhreakSequenceNode;
+import org.drools.core.reteoo.SequenceNode.SequenceNodeMemory;
import org.drools.core.reteoo.SubnetworkTuple;
import org.drools.core.reteoo.TimerNode;
import org.drools.core.reteoo.TimerNode.TimerNodeMemory;
@@ -88,6 +92,7 @@ public class RuleNetworkEvaluatorImpl implements RuleNetworkEvaluator {
private final PhreakBranchNode pBranchNode;
private final PhreakQueryNode pQueryNode;
private final PhreakTimerNode pTimerNode;
+ private final PhreakSequenceNode pSequenceNode;
private final PhreakAsyncSendNode pSendNode;
private final PhreakAsyncReceiveNode pReceiveNode;
private final PhreakRuleTerminalNode pRtNode;
@@ -113,6 +118,7 @@ public class RuleNetworkEvaluatorImpl implements RuleNetworkEvaluator {
pBranchNode = PhreakNetworkNodeFactory.Factory.get().createPhreakBranchNode(reteEvaluator);
pQueryNode = PhreakNetworkNodeFactory.Factory.get().createPhreakQueryNode(reteEvaluator);
pTimerNode = PhreakNetworkNodeFactory.Factory.get().createPhreakTimerNode(reteEvaluator);
+ pSequenceNode = PhreakNetworkNodeFactory.Factory.get().createPhreakSequenceNode(reteEvaluator);
pSendNode = PhreakNetworkNodeFactory.Factory.get().createPhreakAsyncSendNode(reteEvaluator);
pReceiveNode = PhreakNetworkNodeFactory.Factory.get().createPhreakAsyncReceiveNode(reteEvaluator);
pRtNode = PhreakNetworkNodeFactory.Factory.get().createPhreakRuleTerminalNode(reteEvaluator);
@@ -432,6 +438,10 @@ public class RuleNetworkEvaluatorImpl implements RuleNetworkEvaluator {
pReceiveNode.doNode((AsyncReceiveNode) sc.getCurrentNode(), (AsyncReceiveMemory) sc.getCurrentNodeMemory(), sink, sc.getSourceTuples(), trgTuples);
break;
}
+ case NodeTypeEnums.SequenceNode: {
+ pSequenceNode.doNode((SequenceNode) sc.getCurrentNode(), (SequenceNodeMemory) sc.getCurrentNodeMemory(), sink, sc.getSourceTuples(), trgTuples, sc.getStagedLeftTuples());
+ break;
+ }
}
if (exitInnerEval && trgTuples.isEmpty()) {
return null;
diff --git a/drools-core/src/main/java/org/drools/core/reteoo/RightInputAdapterNode.java b/drools-core/src/main/java/org/drools/core/reteoo/RightInputAdapterNode.java
index 11337ad668e..f817f43ee5f 100644
--- a/drools-core/src/main/java/org/drools/core/reteoo/RightInputAdapterNode.java
+++ b/drools-core/src/main/java/org/drools/core/reteoo/RightInputAdapterNode.java
@@ -1,4 +1,4 @@
-/**
+/*
* 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
@@ -406,7 +406,8 @@ public abstract class RightInputAdapterNode<T extends BetaNode> extends BaseNod
@Override
public String toString() {
- return "[RightInputAdapterNode(" + id + ")]";
+ return "RightInputAdapterNode(" + id + ")[ source="
+ + source + ", associations=" + associations + ", partitionId=" + partitionId + "]";
}
public void networkUpdated(UpdateContext updateContext) {
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 26fb206a0b9..2d2c82f4ddd 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
@@ -1162,6 +1162,19 @@ public class SegmentMemory extends LinkedList<SegmentMemory>
}
}
+ public static class SequenceMemoryPrototype extends MemoryPrototype {
+
+ public SequenceMemoryPrototype(long nodePosMaskBit) {
+ this.nodePosMaskBit = nodePosMaskBit;
+ }
+
+ @Override
+ public void populateMemory(NodeMemories nodeMemories, SegmentMemorySupport segmentMemorySupport, Memory mem) {
+ SequenceNode.SequenceNodeMemory seqmem = (SequenceNode.SequenceNodeMemory) mem;
+ seqmem.setNodePosMaskBit(nodePosMaskBit);
+ }
+ }
+
public static class AsyncSendMemoryPrototype extends MemoryPrototype {
public AsyncSendMemoryPrototype() {}
diff --git a/drools-core/src/main/java/org/drools/core/reteoo/SequenceNode.java b/drools-core/src/main/java/org/drools/core/reteoo/SequenceNode.java
new file mode 100644
index 00000000000..d6c78c139af
--- /dev/null
+++ b/drools-core/src/main/java/org/drools/core/reteoo/SequenceNode.java
@@ -0,0 +1,550 @@
+/*
+ * 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.core.reteoo;
+
+import org.drools.base.base.ObjectType;
+import org.drools.base.common.RuleBasePartitionId;
+import org.drools.base.reteoo.DynamicFilter;
+import org.drools.base.reteoo.DynamicFilterProto;
+import org.drools.base.reteoo.NodeTypeEnums;
+import org.drools.base.reteoo.sequencing.Sequence.SequenceMemory;
+import org.drools.base.rule.Pattern;
+import org.drools.core.RuleBaseConfiguration;
+import org.drools.core.common.InternalFactHandle;
+import org.drools.core.common.InternalWorkingMemory;
+import org.drools.core.common.Memory;
+import org.drools.core.common.MemoryFactory;
+import org.drools.core.common.PropagationContext;
+import org.drools.core.common.ReteEvaluator;
+import org.drools.core.common.TupleSets;
+import org.drools.core.common.TupleSetsImpl;
+import org.drools.core.common.UpdateContext;
+import org.drools.core.reteoo.SequenceNode.SequenceNodeMemory;
+import org.drools.core.reteoo.builder.BuildContext;
+import org.drools.base.reteoo.sequencing.Sequencer;
+import org.drools.core.reteoo.sequencing.SequencerMemoryImpl;
+import org.drools.base.reteoo.sequencing.SequencerMemory;
+import org.drools.base.util.AbstractLinkedListNode;
+import org.drools.base.util.LinkedList;
+import org.drools.core.util.index.TupleList;
+import org.drools.util.bitmask.BitMask;
+
+import java.io.Externalizable;
+import java.io.IOException;
+import java.io.ObjectInput;
+import java.io.ObjectOutput;
+import java.util.List;
+
+public class SequenceNode extends LeftTupleSource
+ implements
+ LeftTupleNode,
+ LeftTupleSinkNode,
+ MemoryFactory<SequenceNodeMemory> {
+
+ private static final long serialVersionUID = 510l;
+
+ protected boolean tupleMemoryEnabled;
+
+ private Sequencer sequencer;
+
+ private AlphaAdapter[] alphaAdapters;
+
+ private DynamicFilterProto[] dynamicFilters;
+
+ private LeftTupleSinkNode previousTupleSinkNode;
+
+ private LeftTupleSinkNode nextTupleSinkNode;
+
+ public SequenceNode() {
+
+ }
+
+ public SequenceNode(final int id,
+ final LeftTupleSource tupleSource,
+ final BuildContext context) {
+ super(id, context);
+ setLeftTupleSource(tupleSource);
+ this.setObjectCount(leftInput.getObjectCount()); // 'sequence' nodes do not increase the object count
+ this.tupleMemoryEnabled = context.isTupleMemoryEnabled();
+
+ initMasks(context);
+
+ hashcode = calculateHashCode();
+ setStreamMode(true);
+ }
+
+ public void setAlphaAdapters(AlphaAdapter[] adapters) {
+ this.alphaAdapters = adapters;
+ }
+
+ public DynamicFilterProto[] getDynamicFilters() {
+ return dynamicFilters;
+ }
+
+ public void setDynamicFilters(DynamicFilterProto[] dynamicFilters) {
+ this.dynamicFilters = dynamicFilters;
+ }
+
+ public void doAttach(BuildContext context) {
+ super.doAttach(context);
+ this.leftInput.addTupleSink( this, context );
+ }
+
+ public void networkUpdated(UpdateContext updateContext) {
+ this.leftInput.networkUpdated(updateContext);
+ }
+
+ public String toString() {
+ return "[SequenceNode(" + this.id + ")]]";
+ }
+
+ private int calculateHashCode() {
+ int result = this.leftInput.hashCode();
+ if (this.sequencer != null) {
+ result = 31 * result + System.identityHashCode(this.sequencer.getSequence());
+ }
+ return result;
+ }
+
+ @Override
+ public boolean equals(final Object object) {
+ if (this == object) {
+ return true;
+ }
+
+ if ( object == null || !(object instanceof SequenceNode) || this.hashCode() != object.hashCode() ) {
+ return false;
+ }
+
+ SequenceNode other = (SequenceNode) object;
+ return this.leftInput.getId() == other.leftInput.getId()
+ && this.sequencer.getSequence() == other.sequencer.getSequence();
+ }
+
+ public SequenceNodeMemory createMemory(final RuleBaseConfiguration config, ReteEvaluator reteEvaluator) {
+ LinkedList<DynamicFilter>[] activeFilters = new LinkedList[alphaAdapters.length];
+ SequenceNodeMemory memory = new SequenceNodeMemory(this, activeFilters, new DynamicFilter[dynamicFilters.length]);
+ return memory;
+ }
+
+ public SequencerMemory createSequencerMemory(TupleImpl lt, LeftTupleSink sink, SequenceNodeMemory nodeMemory) {
+ SequencerMemory sequencerMemory = new SequencerMemoryImpl(sequencer, lt, sink, nodeMemory);
+
+ return sequencerMemory;
+ }
+
+ public Sequencer getSequencer() {
+ return sequencer;
+ }
+
+ public void setSequencer(Sequencer sequencer) {
+ this.sequencer = sequencer;
+ this.hashcode = calculateHashCode();
+ }
+
+ public boolean isLeftTupleMemoryEnabled() {
+ return tupleMemoryEnabled;
+ }
+
+ public LeftTupleSinkNode getNextLeftTupleSinkNode() {
+ return this.nextTupleSinkNode;
+ }
+
+ public void setNextLeftTupleSinkNode(final LeftTupleSinkNode next) {
+ this.nextTupleSinkNode = next;
+ }
+
+ public LeftTupleSinkNode getPreviousLeftTupleSinkNode() {
+ return this.previousTupleSinkNode;
+ }
+
+ public void setPreviousLeftTupleSinkNode(final LeftTupleSinkNode previous) {
+ this.previousTupleSinkNode = previous;
+ }
+
+ public int getType() {
+ return NodeTypeEnums.SequenceNode;
+ }
+
+ public static class SequenceNodeMemory extends AbstractLinkedListNode<Memory>
+ implements
+ Externalizable,
+ Memory {
+
+ private static final long serialVersionUID = 510l;
+
+ private TupleMemory leftTupleMemory;
+
+ private TupleSets stagedChildTuples;
+
+ private SegmentMemory memory;
+
+ private LinkedList<DynamicFilter>[] activeFilters;
+
+ private DynamicFilter[] filters;
+
+ private SequenceNode node;
+
+ private long nodePosMaskBit;
+
+ public SequenceNodeMemory(SequenceNode node, LinkedList<DynamicFilter>[] activeFilters, DynamicFilter[] filters) {
+ this.node = node;
+ this.stagedChildTuples = new TupleSetsImpl();
+ this.leftTupleMemory = new TupleList();
+ this.activeFilters = activeFilters;
+ this.filters = filters;
+ }
+
+ public void addActiveFilter(DynamicFilter filter) {
+ int adapterIdx = filter.getAdapterIndex();
+ if (this.activeFilters[adapterIdx] == null) {
+ this.activeFilters[adapterIdx] = new LinkedList<>();
+ }
+ this.activeFilters[adapterIdx].add(filter);
+ }
+
+ public void removeActiveFilter(DynamicFilter filter) {
+ this.activeFilters[filter.getAdapterIndex()].remove(filter);
+ }
+
+ public LinkedList<DynamicFilter>[] getActiveFilters() {
+ return this.activeFilters;
+ }
+
+ public DynamicFilter[] getFilters() {
+ return filters;
+ }
+
+ public void setFilters(DynamicFilter[] filters) {
+ this.filters = filters;
+ }
+
+ public TupleMemory getLeftTupleMemory() {
+ return leftTupleMemory;
+ }
+
+ public TupleSets getStagedChildTuples() {
+ return stagedChildTuples;
+ }
+
+ public void readExternal(ObjectInput in) throws IOException,
+ ClassNotFoundException {
+ throw new UnsupportedOperationException("SequenceNodeMemory does not support serialization");
+ }
+
+ public void writeExternal(ObjectOutput out) throws IOException {
+ throw new UnsupportedOperationException("SequenceNodeMemory does not support serialization");
+ }
+
+ public int getNodeType() {
+ return NodeTypeEnums.SequenceNode;
+ }
+
+ public void setSegmentMemory(SegmentMemory smem) {
+ this.memory = smem;
+ }
+
+ public SegmentMemory getSegmentMemory() {
+ return this.memory;
+ }
+
+ public void reset() {
+ leftTupleMemory.clear();
+ stagedChildTuples.resetAll();
+ for (int i = 0; i < activeFilters.length; i++) {
+ if (activeFilters[i] != null) {
+ activeFilters[i].clear();
+ }
+ }
+ java.util.Arrays.fill(filters, null);
+ }
+
+ public DynamicFilter getActiveDynamicFilter(int filterIndex) {
+ DynamicFilter filter = filters[filterIndex];
+
+ if (filter == null) {
+ DynamicFilterProto proto = node.getDynamicFilters()[filterIndex];
+ filter = new DynamicFilter(proto);
+ filters[filterIndex] = filter;
+ addActiveFilter(filter);
+ } else if (filter.getSignalAdapters().isEmpty()) {
+ addActiveFilter(filter);
+ }
+
+ return filter;
+ }
+
+ public void setNodePosMaskBit(long nodePosMaskBit) {
+ this.nodePosMaskBit = nodePosMaskBit;
+ }
+
+ public long getNodePosMaskBit() {
+ return nodePosMaskBit;
+ }
+ }
+
+ @Override
+ public ObjectTypeNode getObjectTypeNode() {
+ return leftInput.getObjectTypeNode();
+ }
+
+ protected boolean doRemove(final RuleRemovalContext context,
+ final ReteooBuilder builder) {
+ if (!this.isInUse()) {
+ getLeftTupleSource().removeTupleSink(this);
+
+ if (alphaAdapters != null) {
+ for (AlphaAdapter adapter : alphaAdapters) {
+ ((ObjectSource) adapter.getParent()).removeObjectSink(adapter);
+ }
+ }
+ return true;
+ }
+ return false;
+ }
+
+
+ public static class AlphaAdapter extends ObjectSource
+ implements
+ ObjectSinkNode,
+ RightTupleSink {
+ private int adapterIndex;
+
+ private ObjectTypeNodeId otnId;
+
+ private SequenceNode node;
+
+
+ public AlphaAdapter(int id, ObjectSource source, RuleBasePartitionId partitionId, SequenceNode node, int adapterIndex) {
+ super(id, source, partitionId);
+ this.adapterIndex = adapterIndex;
+ this.node = node;
+ }
+
+ public void assertObject(final InternalFactHandle factHandle,
+ final PropagationContext pctx,
+ final ReteEvaluator reteEvaluator) {
+ SequenceNodeMemory memory = reteEvaluator.getNodeMemory(node);
+
+ LinkedList<DynamicFilter> filters = memory.getActiveFilters()[adapterIndex];
+ if (filters != null) {
+ for (DynamicFilter f = filters.getFirst(); f != null; ) {
+ DynamicFilter next = f.getNext();
+ f.assertObject(factHandle, reteEvaluator);
+ f = next;
+ }
+ }
+ }
+
+ public void propagate() {
+
+ }
+
+ public void modifyObject(InternalFactHandle factHandle,
+ ModifyPreviousTuples modifyPreviousTuples,
+ PropagationContext context,
+ ReteEvaluator reteEvaluator) {
+
+ }
+
+ @Override
+ public void retractRightTuple(TupleImpl rightTuple, PropagationContext context, ReteEvaluator reteEvaluator) {
+ // for now assuming we don't have any deletes, but we could add this later.
+ }
+
+ @Override
+ public void modifyRightTuple(TupleImpl rightTuple, PropagationContext context, ReteEvaluator reteEvaluator) {
+ // for now assuming we don't have any modifies, but we could add this later.
+ // typically just gets treated as another add, as the processor is not that stateful (for this use case) - to keep it fast.
+ }
+
+ public int getId() {
+ return super.getId();
+ }
+
+ public void byPassModifyToBetaNode(InternalFactHandle factHandle,
+ ModifyPreviousTuples modifyPreviousTuples,
+ PropagationContext context,
+ ReteEvaluator reteEvaluator) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public ObjectSinkNode getNextObjectSinkNode() {
+ return null;
+ }
+
+ @Override
+ public void setNextObjectSinkNode(ObjectSinkNode next) {
+ // not used — AlphaAdapter is not part of the sink propagation chain
+ }
+
+ @Override
+ public ObjectSinkNode getPreviousObjectSinkNode() {
+ return null;
+ }
+
+ @Override
+ public void setPreviousObjectSinkNode(ObjectSinkNode previous) {
+ // not used — AlphaAdapter is not part of the sink propagation chain
+ }
+
+ @Override
+ public BitMask calculateDeclaredMask(Pattern pattern, ObjectType modifiedType, List<String> settableProperties) {
+ return null;
+ }
+
+ @Override
+ public void updateSink(ObjectSink sink, PropagationContext context, InternalWorkingMemory workingMemory) {
+
+ }
+
+ @Override
+ public ObjectTypeNodeId getInputOtnId() {
+ return otnId;
+ }
+
+ public int getType() {
+ return NodeTypeEnums.SequenceNode;
+ }
+
+ public void doAttach(BuildContext context) {
+ super.doAttach(context);
+ this.source.addObjectSink(this);
+ }
+ }
+
+ public static class PhreakSequenceNode {
+ private final ReteEvaluator reteEvaluator;
+
+ public PhreakSequenceNode(ReteEvaluator reteEvaluator) {
+ this.reteEvaluator = reteEvaluator;
+ }
+
+ public void doNode(SequenceNode node,
+ SequenceNodeMemory memory,
+ LeftTupleSink sink,
+ TupleSets srcLeftTuples,
+ TupleSets trgLeftTuples,
+ TupleSets stagedLeftTuples) {
+ if (srcLeftTuples.getDeleteFirst() != null) {
+ doLeftDeletes(node, srcLeftTuples, trgLeftTuples);
+ }
+
+ if (srcLeftTuples.getUpdateFirst() != null) {
+ doLeftUpdates(node, srcLeftTuples, trgLeftTuples);
+ }
+
+ if (srcLeftTuples.getInsertFirst() != null) {
+ doLeftInserts(node, memory, sink, srcLeftTuples);
+ }
+
+ srcLeftTuples.resetAll();
+
+ if (memory.getStagedChildTuples().getInsertFirst() != null) {
+ for (TupleImpl tuple = memory.getStagedChildTuples().getInsertFirst(); tuple != null; ) {
+ TupleImpl next = tuple.getStagedNext();
+ trgLeftTuples.addInsert(tuple);
+ tuple.clearStaged();
+ tuple = next;
+ }
+
+ memory.getStagedChildTuples().resetAll();
+ }
+
+ memory.getSegmentMemory().updateCleanNodeMask(memory.getNodePosMaskBit());
+ }
+
+ private void doLeftUpdates(SequenceNode node,
+ TupleSets srcLeftTuples,
+ TupleSets trgLeftTuples) {
+ for (TupleImpl leftTuple = srcLeftTuples.getUpdateFirst(); leftTuple != null; ) {
+ TupleImpl next = leftTuple.getStagedNext();
+
+ SequencerMemory sequencerMemory = (SequencerMemory) leftTuple.getContextObject();
+ SequenceMemory sequenceMemory = sequencerMemory.getSequenceMemory(sequencerMemory.getSequencer().getSequence());
+ node.getSequencer().stop(sequenceMemory, reteEvaluator);
+ SequenceNodeMemory nodeMemoryForUpdate = reteEvaluator.getNodeMemory(node);
+ deleteChildren(nodeMemoryForUpdate.getStagedChildTuples(), trgLeftTuples, leftTuple);
+
+ node.getSequencer().start(sequencerMemory, reteEvaluator);
+ leftTuple.getMemory().removeAdd(leftTuple);
+
+ leftTuple.clearStaged();
+ leftTuple = next;
+ }
+ }
+
+ private void doLeftDeletes(SequenceNode node,
+ TupleSets srcLeftTuples,
+ TupleSets trgLeftTuples) {
+ for (TupleImpl leftTuple = srcLeftTuples.getDeleteFirst(); leftTuple != null; ) {
+ TupleImpl next = leftTuple.getStagedNext();
+
+ SequencerMemory sequencerMemory = (SequencerMemory) leftTuple.getContextObject();
+ SequenceNodeMemory nodeMemory = reteEvaluator.getNodeMemory(node);
+
+ SequenceMemory sequenceMemory = sequencerMemory.getSequenceMemory(sequencerMemory.getSequencer().getSequence());
+ node.getSequencer().stop(sequenceMemory, reteEvaluator);
+ leftTuple.getMemory().remove(leftTuple);
+ leftTuple.setContextObject(null);
+
+ deleteChildren(nodeMemory.getStagedChildTuples(), trgLeftTuples, leftTuple);
+
+ leftTuple.clearStaged();
+ leftTuple = next;
+ }
+ }
+
+ private static void deleteChildren(TupleSets stagedChildTuples, TupleSets trgLeftTuples, TupleImpl leftTuple) {
+ if (leftTuple.getFirstChild() != null) {
+ TupleImpl childLeftTuple = leftTuple.getFirstChild();
+ while (childLeftTuple != null) {
+ TupleImpl nextChild = childLeftTuple.getHandleNext();
+ childLeftTuple.unlinkFromLeftParent();
+ if (childLeftTuple.getStagedType() == Tuple.INSERT) {
+ stagedChildTuples.removeInsert(childLeftTuple);
+ } else {
+ trgLeftTuples.addDelete(childLeftTuple);
+ }
+ childLeftTuple = nextChild;
+ }
+ }
+ }
+
+ private void doLeftInserts(SequenceNode node,
+ SequenceNodeMemory memory,
+ LeftTupleSink sink,
+ TupleSets srcLeftTuples) {
+ for (TupleImpl leftTuple = srcLeftTuples.getInsertFirst(); leftTuple != null; ) {
+ TupleImpl next = leftTuple.getStagedNext();
+
+ memory.getLeftTupleMemory().add(leftTuple);
+
+ SequencerMemory sequencerMemory = memory.node.createSequencerMemory(leftTuple, sink, memory);
+ leftTuple.setContextObject(sequencerMemory);
+ node.getSequencer().start(sequencerMemory, reteEvaluator);
+
+ leftTuple.clearStaged();
+ leftTuple = next;
+ }
+ }
+
+ }
+}
diff --git a/drools-core/src/main/java/org/drools/core/reteoo/TupleFactory.java b/drools-core/src/main/java/org/drools/core/reteoo/TupleFactory.java
index adcfae21ec7..a41588da264 100644
--- a/drools-core/src/main/java/org/drools/core/reteoo/TupleFactory.java
+++ b/drools-core/src/main/java/org/drools/core/reteoo/TupleFactory.java
@@ -40,6 +40,7 @@ public class TupleFactory {
case NodeTypeEnums.ConditionalBranchNode:
case NodeTypeEnums.EvalConditionNode:
case NodeTypeEnums.TimerConditionNode:
+ case NodeTypeEnums.SequenceNode:
peer = new EvalNodeLeftTuple();
break;
case NodeTypeEnums.ReactiveFromNode:
@@ -78,6 +79,7 @@ public class TupleFactory {
case NodeTypeEnums.AsyncReceiveNode:
case NodeTypeEnums.ConditionalBranchNode:
case NodeTypeEnums.EvalConditionNode:
+ case NodeTypeEnums.SequenceNode:
case NodeTypeEnums.TimerConditionNode:
return new EvalNodeLeftTuple(factHandle, s, leftTupleMemoryEnabled);
case NodeTypeEnums.ReactiveFromNode:
@@ -109,6 +111,7 @@ public class TupleFactory {
case NodeTypeEnums.ConditionalBranchNode:
case NodeTypeEnums.EvalConditionNode:
case NodeTypeEnums.TimerConditionNode:
+ case NodeTypeEnums.SequenceNode:
return new EvalNodeLeftTuple(factHandle, leftTuple, s);
case NodeTypeEnums.ReactiveFromNode:
return new ReactiveFromNodeLeftTuple(factHandle, leftTuple, s);
@@ -140,6 +143,7 @@ public class TupleFactory {
case NodeTypeEnums.ConditionalBranchNode:
case NodeTypeEnums.EvalConditionNode:
case NodeTypeEnums.TimerConditionNode:
+ case NodeTypeEnums.SequenceNode:
return new EvalNodeLeftTuple(leftTuple, s, pctx, leftTupleMemoryEnabled);
case NodeTypeEnums.ReactiveFromNode:
throw new IllegalStateException("ReactFromNode does not implement this constructor.");
@@ -170,6 +174,7 @@ public class TupleFactory {
case NodeTypeEnums.ConditionalBranchNode:
case NodeTypeEnums.EvalConditionNode:
case NodeTypeEnums.TimerConditionNode:
+ case NodeTypeEnums.SequenceNode:
return new EvalNodeLeftTuple(leftTuple, rightTuple, s);
case NodeTypeEnums.ReactiveFromNode:
throw new IllegalStateException("ReactFromNode does not implement this constructor.");
@@ -203,6 +208,7 @@ public class TupleFactory {
case NodeTypeEnums.ConditionalBranchNode:
case NodeTypeEnums.EvalConditionNode:
case NodeTypeEnums.TimerConditionNode:
+ case NodeTypeEnums.SequenceNode:
return new EvalNodeLeftTuple(leftTuple, rightTuple, currentLeftChild, currentRightChild, s, leftTupleMemoryEnabled);
case NodeTypeEnums.ReactiveFromNode:
return new ReactiveFromNodeLeftTuple(leftTuple, rightTuple, currentLeftChild, currentRightChild, s, leftTupleMemoryEnabled);
diff --git a/drools-core/src/main/java/org/drools/core/reteoo/builder/NodeFactory.java b/drools-core/src/main/java/org/drools/core/reteoo/builder/NodeFactory.java
index 74936b36ff1..cd63e6ae2b5 100644
--- a/drools-core/src/main/java/org/drools/core/reteoo/builder/NodeFactory.java
+++ b/drools-core/src/main/java/org/drools/core/reteoo/builder/NodeFactory.java
@@ -24,6 +24,7 @@ import java.util.List;
import org.drools.base.base.ObjectType;
import org.drools.base.common.RuleBasePartitionId;
import org.drools.base.definitions.rule.impl.RuleImpl;
+import org.drools.base.reteoo.sequencing.Sequence;
import org.drools.base.rule.Accumulate;
import org.drools.base.rule.AsyncReceive;
import org.drools.base.rule.AsyncSend;
@@ -56,6 +57,7 @@ import org.drools.core.reteoo.ObjectTypeNode;
import org.drools.core.reteoo.QueryElementNode;
import org.drools.core.reteoo.ReactiveFromNode;
import org.drools.core.reteoo.TupleToObjectNode;
+import org.drools.core.reteoo.SequenceNode;
import org.drools.core.reteoo.TerminalNode;
import org.drools.core.reteoo.TimerNode;
import org.drools.core.reteoo.WindowNode;
@@ -95,6 +97,10 @@ public interface NodeFactory {
EvalCondition eval,
BuildContext context);
+ SequenceNode buildSequenceNode(int id,
+ LeftTupleSource tupleSource,
+ Sequence seq,
+ BuildContext context);
TupleToObjectNode buildRightInputNode(int id,
LeftTupleSource leftInput,
diff --git a/drools-core/src/main/java/org/drools/core/reteoo/builder/PhreakNodeFactory.java b/drools-core/src/main/java/org/drools/core/reteoo/builder/PhreakNodeFactory.java
index 3087733befd..394434acc54 100644
--- a/drools-core/src/main/java/org/drools/core/reteoo/builder/PhreakNodeFactory.java
+++ b/drools-core/src/main/java/org/drools/core/reteoo/builder/PhreakNodeFactory.java
@@ -20,11 +20,24 @@ package org.drools.core.reteoo.builder;
import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.LinkedHashSet;
import java.util.List;
+import java.util.Map;
+import java.util.Set;
import org.drools.base.base.ObjectType;
import org.drools.base.common.RuleBasePartitionId;
import org.drools.base.definitions.rule.impl.RuleImpl;
+import org.drools.base.reteoo.DynamicFilterProto;
+import org.drools.base.rule.Pattern;
+import org.drools.base.rule.constraint.AlphaNodeFieldConstraint;
+import org.drools.base.rule.constraint.CombinedAlphaConstraint;
+import org.drools.base.rule.constraint.Constraint;
+import org.drools.base.reteoo.sequencing.Sequence;
+import org.drools.base.reteoo.sequencing.Sequencer;
import org.drools.base.rule.Accumulate;
import org.drools.base.rule.AsyncReceive;
import org.drools.base.rule.AsyncSend;
@@ -35,7 +48,6 @@ import org.drools.base.rule.From;
import org.drools.base.rule.GroupElement;
import org.drools.base.rule.QueryElement;
import org.drools.base.rule.accessor.DataProvider;
-import org.drools.base.rule.constraint.AlphaNodeFieldConstraint;
import org.drools.base.time.impl.Timer;
import org.drools.core.common.BetaConstraints;
import org.drools.core.reteoo.AccumulateNode;
@@ -46,6 +58,7 @@ import org.drools.core.reteoo.AsyncReceiveNode;
import org.drools.core.reteoo.AsyncSendNode;
import org.drools.core.reteoo.ConditionalBranchEvaluator;
import org.drools.core.reteoo.ConditionalBranchNode;
+import org.drools.core.reteoo.CoreComponentFactory;
import org.drools.core.reteoo.EntryPointNode;
import org.drools.core.reteoo.EvalConditionNode;
import org.drools.core.reteoo.ExistsNode;
@@ -63,6 +76,8 @@ import org.drools.core.reteoo.QueryElementNode;
import org.drools.core.reteoo.QueryTerminalNode;
import org.drools.core.reteoo.ReactiveFromNode;
import org.drools.core.reteoo.TupleToObjectNode;
+import org.drools.core.reteoo.SequenceNode;
+import org.drools.core.reteoo.SequenceNode.AlphaAdapter;
import org.drools.core.reteoo.RuleTerminalNode;
import org.drools.core.reteoo.TerminalNode;
import org.drools.core.reteoo.TimerNode;
@@ -113,6 +128,83 @@ public class PhreakNodeFactory implements NodeFactory, Serializable {
}
return new TupleToObjectNode(id, leftInput, startTupleSource, context );
}
+ public SequenceNode buildSequenceNode(final int id,
+ final LeftTupleSource tupleSource,
+ final Sequence seq,
+ final BuildContext context) {
+ SequenceNode node = new SequenceNode(id, tupleSource, context);
+ node.setSequencer(new Sequencer(seq));
+
+ NodeFactory nFactory = CoreComponentFactory.get().getNodeFactoryService();
+
+ Pattern[] patterns = seq.getFilters();
+
+ // first attach the OTN sources in Rete.
+ // Use LinkedHashSet to preserve insertion order (first-seen per type),
+ // so objectTypeList index aligns with the first pattern index for that type.
+ Set<ObjectType> objectTypeSet = new LinkedHashSet<>();
+ Arrays.stream(patterns).forEach(p -> objectTypeSet.add(p.getObjectType()));
+ List<ObjectType> objectTypeList = new ArrayList<>(objectTypeSet);
+
+ Map<ObjectType, Integer> objectTypeIndex = new HashMap<>(objectTypeList.size() * 2);
+ ObjectSource[] otns = new ObjectSource[objectTypeList.size()];
+ EntryPointNode epn = context.getRuleBase().getRete().getEntryPointNode(context.getCurrentEntryPoint());
+ Map<ObjectType, ObjectTypeNode> existingOtns = epn.getObjectTypeNodes();
+ for ( int i = 0; i < objectTypeList.size(); i++ ) {
+ ObjectType objectType = objectTypeList.get(i);
+ objectTypeIndex.put(objectType, i);
+ ObjectTypeNode existing = existingOtns.get(objectType);
+ if (existing != null) {
+ // Reuse the existing OTN — building a fresh one and calling attach()
+ // would call EntryPointNode.addObjectSink which is a Map.put, silently
+ // replacing the existing OTN and cutting off all rules already wired to it.
+ otns[i] = existing;
+ } else {
+ otns[i] = nFactory.buildObjectTypeNode(context.getNextNodeId(), epn, objectType, context);
+ otns[i].attach(context);
+ }
+ }
+
+ // For each source create an alpha adapter, which will map it to any filters on the stack.
+ AlphaAdapter[] adapters = new AlphaAdapter[objectTypeList.size()];
+ for ( int i = 0; i < objectTypeList.size(); i++ ) {
+ adapters[i] = new AlphaAdapter(context.getNextNodeId(), otns[i],
+ context.getPartitionId(), node, i);
+
+ adapters[i].attach(context);
+ }
+ node.setAlphaAdapters(adapters);
+
+
+ // Create all the dynamic filters, these are added on demand to the stack.
+ // filterIndex (i) is the pattern index — one per step.
+ // adapterIndex is the index of this pattern's type in objectTypeList — one per distinct type.
+ DynamicFilterProto[] filters = new DynamicFilterProto[patterns.length];
+ for ( int i = 0; i < patterns.length; i++ ) {
+ List<Constraint> constraints = patterns[i].getConstraints();
+ if (constraints.isEmpty()) {
+ throw new IllegalArgumentException(
+ "sequence step " + i + " has no constraint expression. " +
+ "Every sequence step must have at least one expr(). " +
+ "Add an expr() to the pattern, or use a dedicated fact type per step.");
+ }
+ AlphaNodeFieldConstraint combined;
+ if (constraints.size() == 1) {
+ combined = (AlphaNodeFieldConstraint) constraints.get(0);
+ } else {
+ AlphaNodeFieldConstraint[] all = constraints.stream()
+ .map(c -> (AlphaNodeFieldConstraint) c)
+ .toArray(AlphaNodeFieldConstraint[]::new);
+ combined = new CombinedAlphaConstraint(all);
+ }
+ int adapterIndex = objectTypeIndex.get(patterns[i].getObjectType());
+ filters[i] = new DynamicFilterProto(combined, adapterIndex);
+ }
+
+ node.setDynamicFilters( filters);
+
+ return node;
+ }
public JoinNode buildJoinNode( int id, LeftTupleSource leftInput, ObjectSource rightInput, BetaConstraints binder, BuildContext context ) {
JoinRightAdapterNode joinRight = new JoinRightAdapterNode(id, rightInput, context);
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 6403061d31f..e6f47d468a2 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
@@ -25,6 +25,7 @@ import java.util.List;
import org.drools.base.base.ClassObjectType;
import org.drools.base.definitions.rule.impl.RuleImpl;
import org.drools.base.reteoo.NodeTypeEnums;
+import org.drools.base.reteoo.sequencing.Sequence;
import org.drools.base.rule.Accumulate;
import org.drools.base.rule.AsyncReceive;
import org.drools.base.rule.AsyncSend;
@@ -70,6 +71,8 @@ public class ReteooRuleBuilder implements RuleBuilder {
new PatternBuilder() );
this.utils.addBuilder( EvalCondition.class,
new EvalBuilder() );
+ this.utils.addBuilder( Sequence.class,
+ new SequenceBuilder() );
this.utils.addBuilder( QueryElement.class,
new QueryElementBuilder() );
this.utils.addBuilder( From.class,
diff --git a/drools-core/src/main/java/org/drools/core/reteoo/builder/SequenceBuilder.java b/drools-core/src/main/java/org/drools/core/reteoo/builder/SequenceBuilder.java
new file mode 100755
index 00000000000..bd5d2edaa33
--- /dev/null
+++ b/drools-core/src/main/java/org/drools/core/reteoo/builder/SequenceBuilder.java
@@ -0,0 +1,55 @@
+/*
+ * 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.core.reteoo.builder;
+
+import org.drools.base.reteoo.sequencing.Sequence;
+import org.drools.base.rule.RuleConditionElement;
+import org.drools.core.reteoo.CoreComponentFactory;
+
+public class SequenceBuilder
+ implements
+ ReteooComponentBuilder {
+
+ /**
+ * @inheritDoc
+ */
+ public void build(final BuildContext context,
+ final BuildUtils utils,
+ final RuleConditionElement rce) {
+ final Sequence seq = (Sequence) rce;
+ context.pushRuleComponent( rce );
+ context.setTupleSource( utils.attachNode( context,
+ CoreComponentFactory.get()
+ .getNodeFactoryService().buildSequenceNode(context.getNextNodeId(),
+ context.getTupleSource(),
+ seq,
+ context) ) );
+
+ context.popRuleComponent();
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public boolean requiresLeftActivation(final BuildUtils utils,
+ final RuleConditionElement rce) {
+ return true;
+ }
+
+}
diff --git a/drools-core/src/main/java/org/drools/core/reteoo/sequencing/SequencerMemoryImpl.java b/drools-core/src/main/java/org/drools/core/reteoo/sequencing/SequencerMemoryImpl.java
new file mode 100644
index 00000000000..26b663e92d6
--- /dev/null
+++ b/drools-core/src/main/java/org/drools/core/reteoo/sequencing/SequencerMemoryImpl.java
@@ -0,0 +1,152 @@
+/*
+ * 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.core.reteoo.sequencing;
+
+import org.drools.base.base.ValueResolver;
+import org.drools.base.reteoo.DynamicFilter;
+import org.drools.base.reteoo.sequencing.Sequence;
+import org.drools.base.reteoo.sequencing.Sequencer;
+import org.drools.base.reteoo.sequencing.SequencerMemory;
+import org.drools.core.common.ReteEvaluator;
+import org.drools.core.common.TupleSetsImpl;
+import org.drools.core.reteoo.LeftTupleSink;
+import org.drools.core.reteoo.PathMemory;
+import org.drools.core.reteoo.SegmentMemory;
+import org.drools.core.reteoo.SequenceNode;
+import org.drools.core.reteoo.SequenceNode.SequenceNodeMemory;
+import org.drools.base.reteoo.SignalAdapter;
+import org.drools.core.reteoo.TupleFactory;
+import org.drools.core.reteoo.TupleImpl;
+import org.drools.base.reteoo.sequencing.Sequence.SequenceMemory;
+import org.drools.base.reteoo.sequencing.signalprocessors.LogicGate;
+import org.drools.base.util.CircularArrayList;
+
+
+public class SequencerMemoryImpl implements SequencerMemory {
+
+ private final TupleImpl lt;
+
+ private final CircularArrayList<Object> events;
+
+ private final SequenceMemory[] sequenceMemories;
+
+ private final LeftTupleSink sink;
+
+ private Sequencer sequencer;
+
+ private SequenceNodeMemory nodeMemory;
+
+ private SequenceMemory childSequenceMemory;
+
+ public SequencerMemoryImpl(Sequencer sequencer, TupleImpl lt, LeftTupleSink sink, SequenceNodeMemory nodeMemory) {
+ this.sequencer = sequencer;
+ this.lt = lt;
+ int filterCount = sequencer.getSequence().getFilters().length;
+ this.events = new CircularArrayList<>(Object.class, Math.max(filterCount, 4));
+ this.sink = sink;
+ this.sequenceMemories = new SequenceMemory[sequencer.getSequences().length];
+ this.nodeMemory = nodeMemory;
+ }
+
+ @Override
+ public TupleImpl getLeftTuple() {
+ return lt;
+ }
+
+ @Override
+ public CircularArrayList<Object> getData() {
+ return events;
+ }
+
+ @Override
+ public Sequencer getSequencer() {
+ return sequencer;
+ }
+
+ @Override
+ public SequenceMemory getChildSequenceMemory() {
+ return childSequenceMemory;
+ }
+
+ @Override
+ public void setChildSequenceMemory(SequenceMemory childSequenceMemory) {
+ this.childSequenceMemory = childSequenceMemory;
+ }
+
+ public SequenceMemory getOrCreateSequenceMemory(Sequence sequence, CircularArrayList<Object> newData) {
+ SequenceMemory sequenceMemory = sequenceMemories[sequence.getSequenceIndex()];
+ if (sequenceMemory == null) {
+
+ int signalAdapters = 0;
+
+ for (LogicGate gate : sequence.getGates()) {
+ signalAdapters = signalAdapters + gate.getSignalAdapterIndexes().length;
+ }
+
+ long[] gateMemory = new long[sequence.getGates().length];
+ long[] counterMemory = new long[0];
+
+ sequenceMemory = new SequenceMemory(this, sequence, newData,
+ new SignalAdapter[signalAdapters], new SignalAdapter[signalAdapters],
+ gateMemory, counterMemory);
+
+ sequenceMemories[sequence.getSequenceIndex()] = sequenceMemory;
+ }
+
+ return sequenceMemory;
+ }
+
+ @Override
+ public SequenceMemory getSequenceMemory(Sequence sequence) {
+ SequenceMemory sequenceMemory = sequenceMemories[sequence.getSequenceIndex()];
+ return sequenceMemory;
+ }
+
+
+ @Override
+ public void match(ValueResolver valueResolver) {
+ boolean wasEmpty = nodeMemory.getStagedChildTuples().isEmpty();
+ TupleImpl child = TupleFactory.createLeftTuple(lt, sink, lt.getPropagationContext(), true);
+ nodeMemory.getStagedChildTuples().addInsert(child);
+
+ long nodePosMaskBit = nodeMemory.getNodePosMaskBit();
+ SegmentMemory smem = nodeMemory.getSegmentMemory();
+
+ if (wasEmpty) {
+ smem.notifyRuleLinkSegment(nodePosMaskBit);
+ } else {
+ smem.linkSegmentWithoutRuleNotify(nodePosMaskBit);
+ }
+
+ ReteEvaluator reteEvaluator = (ReteEvaluator) valueResolver;
+ PathMemory pmem = smem.getPathMemories().get(0);
+ reteEvaluator.getRuleNetworkEvaluator().forceFlushLeftTuple(pmem, smem, new TupleSetsImpl());
+ reteEvaluator.getRuleNetworkEvaluator().forceFlushWhenSubnetwork(pmem);
+ }
+
+ @Override
+ public DynamicFilter getActiveDynamicFilter(int filterIndex) {
+ return nodeMemory.getActiveDynamicFilter(filterIndex);
+ }
+
+ @Override
+ public void removeActiveFilter(DynamicFilter filter) {
+ nodeMemory.removeActiveFilter(filter);
+ }
+}
diff --git a/drools-metric/pom.xml b/drools-metric/pom.xml
index 3e66126705d..dfcd0e89d8a 100644
--- a/drools-metric/pom.xml
+++ b/drools-metric/pom.xml
@@ -73,6 +73,11 @@
<type>test-jar</type>
<scope>test</scope>
</dependency>
+ <dependency>
+ <groupId>org.drools</groupId>
+ <artifactId>drools-model-compiler</artifactId>
+ <scope>test</scope>
+ </dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
diff --git a/drools-metric/src/main/java/org/drools/metric/phreak/MetricPhreakNetworkNodeFactoryImpl.java b/drools-metric/src/main/java/org/drools/metric/phreak/MetricPhreakNetworkNodeFactoryImpl.java
index 67475c99204..82cd1461618 100644
--- a/drools-metric/src/main/java/org/drools/metric/phreak/MetricPhreakNetworkNodeFactoryImpl.java
+++ b/drools-metric/src/main/java/org/drools/metric/phreak/MetricPhreakNetworkNodeFactoryImpl.java
@@ -35,6 +35,7 @@ import org.drools.core.phreak.PhreakQueryTerminalNode;
import org.drools.core.phreak.PhreakReactiveFromNode;
import org.drools.core.phreak.PhreakRuleTerminalNode;
import org.drools.core.phreak.PhreakTimerNode;
+import org.drools.core.reteoo.SequenceNode.PhreakSequenceNode;
import org.drools.metric.util.MetricLogUtils;
public class MetricPhreakNetworkNodeFactoryImpl implements PhreakNetworkNodeFactory {
@@ -138,6 +139,15 @@ public class MetricPhreakNetworkNodeFactoryImpl implements PhreakNetworkNodeFact
}
}
+ @Override
+ public PhreakSequenceNode createPhreakSequenceNode(ReteEvaluator reteEvaluator) {
+ if (MetricLogUtils.getInstance().isEnabled()) {
+ return new PhreakSequenceNodeMetric(reteEvaluator);
+ } else {
+ return new PhreakSequenceNode(reteEvaluator);
+ }
+ }
+
@Override
public PhreakAsyncSendNode createPhreakAsyncSendNode(ReteEvaluator reteEvaluator) {
if (MetricLogUtils.getInstance().isEnabled()) {
diff --git a/drools-metric/src/main/java/org/drools/metric/phreak/PhreakSequenceNodeMetric.java b/drools-metric/src/main/java/org/drools/metric/phreak/PhreakSequenceNodeMetric.java
new file mode 100644
index 00000000000..ab458fa98b8
--- /dev/null
+++ b/drools-metric/src/main/java/org/drools/metric/phreak/PhreakSequenceNodeMetric.java
@@ -0,0 +1,51 @@
+/*
+ * 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.metric.phreak;
+
+import org.drools.core.common.ReteEvaluator;
+import org.drools.core.common.TupleSets;
+import org.drools.core.reteoo.LeftTupleSink;
+import org.drools.core.reteoo.SequenceNode;
+import org.drools.core.reteoo.SequenceNode.SequenceNodeMemory;
+import org.drools.metric.util.MetricLogUtils;
+
+public class PhreakSequenceNodeMetric extends SequenceNode.PhreakSequenceNode {
+
+ public PhreakSequenceNodeMetric(ReteEvaluator reteEvaluator) {
+ super(reteEvaluator);
+ }
+
+ @Override
+ public void doNode(SequenceNode node,
+ SequenceNodeMemory memory,
+ LeftTupleSink sink,
+ TupleSets srcLeftTuples,
+ TupleSets trgLeftTuples,
+ TupleSets stagedLeftTuples) {
+ try {
+ MetricLogUtils.getInstance().startMetrics(node);
+ MetricLogUtils.getInstance().incrementEvalCount();
+
+ super.doNode(node, memory, sink, srcLeftTuples, trgLeftTuples, stagedLeftTuples);
+
+ } finally {
+ MetricLogUtils.getInstance().logAndEndMetrics();
+ }
+ }
+}
diff --git a/drools-metric/src/test/java/org/drools/metric/SequenceNodeMetricTest.java b/drools-metric/src/test/java/org/drools/metric/SequenceNodeMetricTest.java
new file mode 100644
index 00000000000..370ec14a4f3
--- /dev/null
+++ b/drools-metric/src/test/java/org/drools/metric/SequenceNodeMetricTest.java
@@ -0,0 +1,126 @@
+/*
+ * 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.metric;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+import io.micrometer.core.instrument.Timer;
+import io.micrometer.core.instrument.search.Search;
+import org.drools.model.Model;
+import org.drools.model.Rule;
+import org.drools.model.Variable;
+import org.drools.model.impl.ModelImpl;
+import org.drools.modelcompiler.KieBaseBuilder;
+import org.junit.jupiter.api.Test;
+import org.kie.api.KieBase;
+import org.kie.api.runtime.KieSession;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.drools.model.DSL.declarationOf;
+import static org.drools.model.DSL.execute;
+import static org.drools.model.PatternDSL.pattern;
+import static org.drools.model.PatternDSL.rule;
+import static org.drools.model.PatternDSL.sequence;
+
+public class SequenceNodeMetricTest extends AbstractMetricTest {
+
+ /**
+ * Builds a KieBase with a single rule whose LHS is: String() then sequence(Integer(), Long()).
+ * Inserting a String activates the sequencer; inserting an Integer then a Long fires the rule.
+ */
+ private KieSession buildSequenceSession(List<String> results) {
+ Variable<String> anchor = declarationOf(String.class);
+ Variable<Integer> step1 = declarationOf(Integer.class);
+ Variable<Long> step2 = declarationOf(Long.class);
+
+ Rule r = rule("sequence-metric-rule").build(
+ pattern(anchor),
+ sequence(
+ pattern(step1).expr("step1-expr", i -> i > 0),
+ pattern(step2).expr("step2-expr", l -> l > 0)
+ ),
+ execute(() -> results.add("fired"))
+ );
+
+ Model model = new ModelImpl().addRule(r);
+ KieBase kieBase = KieBaseBuilder.createKieBaseFromModel(model);
+ return kieBase.newKieSession();
+ }
+
+ @Test
+ public void sequenceNodeMetricIsRecordedWhenMetricsEnabled() {
+ List<String> results = new ArrayList<>();
+ KieSession ksession = buildSequenceSession(results);
+
+ // Activate sequencer
+ ksession.insert("anchor");
+ ksession.fireAllRules();
+
+ // Advance sequence: step 1
+ ksession.insert(1);
+ ksession.fireAllRules();
+
+ // Advance sequence: step 2 — rule fires
+ ksession.insert(1L);
+ ksession.fireAllRules();
+
+ ksession.dispose();
+
+ assertThat(results).containsExactly("fired");
+
+ // Metrics must have been recorded for the SequenceNode
+ Collection<Timer> timers = Search.in(registry)
+ .name("org.drools.metric.elapsed.time.per.evaluation")
+ .timers();
+ assertThat(timers).isNotEmpty();
+ }
+
+ @Test
+ public void sequenceNodeMetricIsNotRecordedWhenMetricsDisabled() {
+ // Override: disable metrics for this test
+ System.setProperty("drools.metric.logger.enabled", "false");
+ org.drools.metric.util.MetricLogUtils.recreateInstance();
+
+ try {
+ List<String> results = new ArrayList<>();
+ KieSession ksession = buildSequenceSession(results);
+
+ ksession.insert("anchor");
+ ksession.fireAllRules();
+ ksession.insert(1);
+ ksession.fireAllRules();
+ ksession.insert(1L);
+ ksession.fireAllRules();
+ ksession.dispose();
+
+ assertThat(results).containsExactly("fired");
+
+ // No timers should be registered when metrics are disabled
+ Collection<Timer> timers = Search.in(registry)
+ .name("org.drools.metric.elapsed.time.per.evaluation")
+ .timers();
+ assertThat(timers).isEmpty();
+ } finally {
+ System.setProperty("drools.metric.logger.enabled", "true");
+ org.drools.metric.util.MetricLogUtils.recreateInstance();
+ }
+ }
+}
diff --git a/drools-model/drools-canonical-model/src/main/java/org/drools/model/Condition.java b/drools-model/drools-canonical-model/src/main/java/org/drools/model/Condition.java
index 6897ad2478f..21e85fc9fc2 100644
--- a/drools-model/drools-canonical-model/src/main/java/org/drools/model/Condition.java
+++ b/drools-model/drools-canonical-model/src/main/java/org/drools/model/Condition.java
@@ -39,7 +39,7 @@ public interface Condition {
PATTERN( false ), EVAL( false ), QUERY( false ),
ACCUMULATE( false ), GROUP_BY( false ), TEMPORAL( false ),
RECEIVER( false ), SENDER( false ),
- OR( true ), AND( true ), NOT( false ), EXISTS( false ), FORALL( false ), CONSEQUENCE( false );
+ OR( true ), AND( true ), NOT( false ), EXISTS( false ), FORALL( false ), SEQUENCE( false ), CONSEQUENCE( false );
private final boolean composite;
diff --git a/drools-model/drools-canonical-model/src/main/java/org/drools/model/PatternDSL.java b/drools-model/drools-canonical-model/src/main/java/org/drools/model/PatternDSL.java
index d49629f1862..1023077e5e6 100644
--- a/drools-model/drools-canonical-model/src/main/java/org/drools/model/PatternDSL.java
+++ b/drools-model/drools-canonical-model/src/main/java/org/drools/model/PatternDSL.java
@@ -93,8 +93,10 @@ import org.drools.model.view.BindViewItem1;
import org.drools.model.view.BindViewItem2;
import org.drools.model.view.BindViewItem3;
import org.drools.model.view.BindViewItem4;
+import org.drools.model.view.CombinedExprViewItem;
import org.drools.model.view.ExprViewItem;
import org.drools.model.view.ViewItem;
+import org.drools.model.view.ViewItemBuilder;
import static java.util.UUID.randomUUID;
@@ -142,7 +144,7 @@ public class PatternDSL extends DSL {
return new ReactOn( reactOn );
}
- public interface PatternDef<T> extends ViewItem<T> {
+ public interface PatternDef<T> extends ViewItem<T>, SequenceStep {
PatternDef<T> and();
PatternDef<T> or();
PatternDef<T> endAnd();
@@ -1775,6 +1777,23 @@ public class PatternDSL extends DSL {
}
}
+ public static class SequenceViewItem implements RuleItemBuilder<SequenceViewItem>, RuleItem {
+ private final SequenceStep[] steps;
+
+ public SequenceViewItem(SequenceStep... steps) {
+ this.steps = steps;
+ }
+
+ public SequenceStep[] getSteps() {
+ return steps;
+ }
+
+ @Override
+ public SequenceViewItem get() {
+ return this;
+ }
+ }
+
public static abstract class PatternBindingImpl<T, A> implements PatternItem<T> {
protected final Variable<A> boundVar;
protected final ReactOn reactOn;
@@ -1887,6 +1906,32 @@ public class PatternDSL extends DSL {
// -- rule --
+ public static SequenceViewItem sequence(SequenceStep... steps) {
+ if (steps == null || steps.length == 0) {
+ throw new IllegalArgumentException("sequence() requires at least one step");
+ }
+ return new SequenceViewItem(steps);
+ }
+
+
+ public static CombinedExprViewItem or(ViewItemBuilder<?> first, ViewItemBuilder<?>... rest) {
+ return new CombinedExprViewItem(Condition.Type.OR, viewItems(first, rest));
+ }
+
+ public static CombinedExprViewItem and(ViewItemBuilder<?> first, ViewItemBuilder<?>... rest) {
+ return new CombinedExprViewItem(Condition.Type.AND, viewItems(first, rest));
+ }
+
+ private static ViewItem[] viewItems(ViewItemBuilder<?> first, ViewItemBuilder<?>[] rest) {
+ ViewItem[] all = new ViewItem[1 + rest.length];
+ all[0] = first.get();
+ for (int i = 0; i < rest.length; i++) {
+ all[1 + i] = rest[i].get();
+ }
+ return all;
+ }
+
+
public static RuleBuilder rule( String name ) {
return new RuleBuilder( VIEW_BUILDER, name );
}
diff --git a/drools-model/drools-canonical-model/src/main/java/org/drools/model/view/ExistentialExprViewItem.java b/drools-model/drools-canonical-model/src/main/java/org/drools/model/SequenceStep.java
similarity index 53%
copy from drools-model/drools-canonical-model/src/main/java/org/drools/model/view/ExistentialExprViewItem.java
copy to drools-model/drools-canonical-model/src/main/java/org/drools/model/SequenceStep.java
index d74396d1070..c785029648d 100644
--- a/drools-model/drools-canonical-model/src/main/java/org/drools/model/view/ExistentialExprViewItem.java
+++ b/drools-model/drools-canonical-model/src/main/java/org/drools/model/SequenceStep.java
@@ -16,33 +16,11 @@
* specific language governing permissions and limitations
* under the License.
*/
-package org.drools.model.view;
+package org.drools.model;
-import org.drools.model.Condition;
-import org.drools.model.Variable;
-
-public class ExistentialExprViewItem<T> extends AbstractExprViewItem<T> {
-
- private final Condition.Type type;
- private final ViewItem expression;
-
- public ExistentialExprViewItem(Condition.Type type, ViewItem expression) {
- super(expression.getFirstVariable());
- this.type = type;
- this.expression = expression;
- }
-
- public ViewItem getExpression() {
- return expression;
- }
-
- @Override
- public Variable<?>[] getVariables() {
- return expression.getVariables();
- }
-
- @Override
- public Condition.Type getType() {
- return type;
- }
+/**
+ * Marker for items that may appear as a step inside {@code sequence(...)}.
+ * Stamped on PatternDef, CombinedExprViewItem, and ExistentialExprViewItem.
+ */
+public interface SequenceStep {
}
diff --git a/drools-model/drools-canonical-model/src/main/java/org/drools/model/impl/ViewPatternBuilder.java b/drools-model/drools-canonical-model/src/main/java/org/drools/model/impl/ViewPatternBuilder.java
index f9d1cb7190d..c16411e3b71 100644
--- a/drools-model/drools-canonical-model/src/main/java/org/drools/model/impl/ViewPatternBuilder.java
+++ b/drools-model/drools-canonical-model/src/main/java/org/drools/model/impl/ViewPatternBuilder.java
@@ -19,6 +19,7 @@
package org.drools.model.impl;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
@@ -33,6 +34,7 @@ import org.drools.model.PatternDSL.PatternBindingImpl;
import org.drools.model.PatternDSL.PatternDefImpl;
import org.drools.model.PatternDSL.PatternExprImpl;
import org.drools.model.PatternDSL.PatternItem;
+import org.drools.model.PatternDSL.SequenceViewItem;
import org.drools.model.RuleItem;
import org.drools.model.RuleItemBuilder;
import org.drools.model.SingleConstraint;
@@ -59,6 +61,7 @@ import org.drools.model.patterns.ExistentialPatternImpl;
import org.drools.model.patterns.GroupByPatternImpl;
import org.drools.model.patterns.PatternImpl;
import org.drools.model.patterns.QueryCallPattern;
+import org.drools.model.patterns.SequenceConditionImpl;
import org.drools.model.view.AccumulateExprViewItem;
import org.drools.model.view.CombinedExprViewItem;
import org.drools.model.view.ExistentialExprViewItem;
@@ -180,6 +183,14 @@ public class ViewPatternBuilder implements ViewBuilder {
return new PatternImpl( exchangeDef.getFirstVariable(), Condition.Type.SENDER );
}
+ if (ruleItem instanceof SequenceViewItem) {
+ SequenceViewItem sv = (SequenceViewItem) ruleItem;
+ List<Condition> steps = Arrays.stream(sv.getSteps())
+ .map(s -> ruleItem2Condition((RuleItem) s))
+ .collect(toList());
+ return new SequenceConditionImpl(steps);
+ }
+
throw new UnsupportedOperationException( "Unknown " + ruleItem );
}
diff --git a/drools-model/drools-canonical-model/src/main/java/org/drools/model/view/ExistentialExprViewItem.java b/drools-model/drools-canonical-model/src/main/java/org/drools/model/patterns/SequenceConditionImpl.java
similarity index 61%
copy from drools-model/drools-canonical-model/src/main/java/org/drools/model/view/ExistentialExprViewItem.java
copy to drools-model/drools-canonical-model/src/main/java/org/drools/model/patterns/SequenceConditionImpl.java
index d74396d1070..3e938431e41 100644
--- a/drools-model/drools-canonical-model/src/main/java/org/drools/model/view/ExistentialExprViewItem.java
+++ b/drools-model/drools-canonical-model/src/main/java/org/drools/model/patterns/SequenceConditionImpl.java
@@ -16,33 +16,33 @@
* specific language governing permissions and limitations
* under the License.
*/
-package org.drools.model.view;
+package org.drools.model.patterns;
+
+import java.util.List;
import org.drools.model.Condition;
import org.drools.model.Variable;
-public class ExistentialExprViewItem<T> extends AbstractExprViewItem<T> {
+public class SequenceConditionImpl implements Condition {
- private final Condition.Type type;
- private final ViewItem expression;
+ private final List<Condition> steps;
- public ExistentialExprViewItem(Condition.Type type, ViewItem expression) {
- super(expression.getFirstVariable());
- this.type = type;
- this.expression = expression;
+ public SequenceConditionImpl(List<Condition> steps) {
+ this.steps = steps;
}
- public ViewItem getExpression() {
- return expression;
+ @Override
+ public Type getType() {
+ return Type.SEQUENCE;
}
@Override
- public Variable<?>[] getVariables() {
- return expression.getVariables();
+ public List<Condition> getSubConditions() {
+ return steps;
}
@Override
- public Condition.Type getType() {
- return type;
+ public Variable<?>[] getBoundVariables() {
+ return new Variable[0];
}
}
diff --git a/drools-model/drools-canonical-model/src/main/java/org/drools/model/view/CombinedExprViewItem.java b/drools-model/drools-canonical-model/src/main/java/org/drools/model/view/CombinedExprViewItem.java
index 08cbf80fb3f..91d6aa4c099 100644
--- a/drools-model/drools-canonical-model/src/main/java/org/drools/model/view/CombinedExprViewItem.java
+++ b/drools-model/drools-canonical-model/src/main/java/org/drools/model/view/CombinedExprViewItem.java
@@ -23,7 +23,7 @@ import java.util.stream.Stream;
import org.drools.model.Condition;
import org.drools.model.Variable;
-public class CombinedExprViewItem<T> extends AbstractExprViewItem<T> {
+public class CombinedExprViewItem<T> extends AbstractExprViewItem<T> implements org.drools.model.SequenceStep {
private final Condition.Type type;
private final ViewItem[] expressions;
diff --git a/drools-model/drools-canonical-model/src/main/java/org/drools/model/view/ExistentialExprViewItem.java b/drools-model/drools-canonical-model/src/main/java/org/drools/model/view/ExistentialExprViewItem.java
index d74396d1070..23fd4ff3c3b 100644
--- a/drools-model/drools-canonical-model/src/main/java/org/drools/model/view/ExistentialExprViewItem.java
+++ b/drools-model/drools-canonical-model/src/main/java/org/drools/model/view/ExistentialExprViewItem.java
@@ -21,7 +21,7 @@ package org.drools.model.view;
import org.drools.model.Condition;
import org.drools.model.Variable;
-public class ExistentialExprViewItem<T> extends AbstractExprViewItem<T> {
+public class ExistentialExprViewItem<T> extends AbstractExprViewItem<T> implements org.drools.model.SequenceStep {
private final Condition.Type type;
private final ViewItem expression;
diff --git a/drools-model/drools-model-codegen/src/test/java/org/drools/model/codegen/execmodel/ReteDumperTest.java b/drools-model/drools-model-codegen/src/test/java/org/drools/model/codegen/execmodel/ReteDumperTest.java
index da9cad76a27..606c5879eaa 100644
--- a/drools-model/drools-model-codegen/src/test/java/org/drools/model/codegen/execmodel/ReteDumperTest.java
+++ b/drools-model/drools-model-codegen/src/test/java/org/drools/model/codegen/execmodel/ReteDumperTest.java
@@ -58,7 +58,7 @@ class ReteDumperTest extends BaseModelTest {
List<String> lines = getLinesByCoreReteDumper(kbase);
// The first RightInputAdapterNode should be followed by ExistsNode
- assertSinkOrder(lines, "[RightInputAdapterNode", "[ExistsNode");
+ assertSinkOrder(lines, "RightInputAdapterNode", "[ExistsNode");
}
@ParameterizedTest
@@ -70,7 +70,7 @@ class ReteDumperTest extends BaseModelTest {
List<String> lines = getLinesByExecmodelReteDumper(kbase);
// The first RightInputAdapterNode should be followed by ExistsNode
- assertSinkOrder(lines, "[RightInputAdapterNode", "[ExistsNode");
+ assertSinkOrder(lines, "RightInputAdapterNode", "[ExistsNode");
}
private static void assertSinkOrder(List<String> lines, String targetNodePrefix, String expectedNextNodePrefix) {
diff --git a/drools-model/drools-model-compiler/src/main/java/org/drools/modelcompiler/KiePackagesBuilder.java b/drools-model/drools-model-compiler/src/main/java/org/drools/modelcompiler/KiePackagesBuilder.java
index 332cc0387b6..d80d740f481 100644
--- a/drools-model/drools-model-compiler/src/main/java/org/drools/modelcompiler/KiePackagesBuilder.java
+++ b/drools-model/drools-model-compiler/src/main/java/org/drools/modelcompiler/KiePackagesBuilder.java
@@ -131,6 +131,15 @@ import org.drools.model.patterns.ExistentialPatternImpl;
import org.drools.model.patterns.GroupByPatternImpl;
import org.drools.model.patterns.PatternImpl;
import org.drools.model.patterns.QueryCallPattern;
+import org.drools.model.patterns.SequenceConditionImpl;
+import org.drools.base.reteoo.sequencing.Sequence;
+import org.drools.base.reteoo.sequencing.signalprocessors.Gates;
+import org.drools.base.reteoo.sequencing.signalprocessors.LogicCircuit;
+import org.drools.base.reteoo.sequencing.signalprocessors.LogicGate;
+import org.drools.base.reteoo.sequencing.signalprocessors.LogicGateOutputSignalProcessor;
+import org.drools.base.reteoo.sequencing.signalprocessors.SignalIndex;
+import org.drools.base.reteoo.sequencing.signalprocessors.TerminatingSignalProcessor;
+import org.drools.base.reteoo.sequencing.steps.Step;
import org.drools.model.view.SelfPatternBiding;
import org.drools.modelcompiler.attributes.LambdaEnabled;
import org.drools.modelcompiler.attributes.LambdaSalience;
@@ -398,15 +407,41 @@ public class KiePackagesBuilder {
Declaration[] requiredDeclarations = getRequiredDeclarationsIfPossible( ctx, consequence, name );
boolean enabledTupleOptimization = requiredDeclarations != null && requiredDeclarations.length > 0;
+ Map<String, int[]> sequenceVarIndexes = buildSequenceVarIndexesForConsequence( ctx, consequence );
+
if ( name.equals( RuleImpl.DEFAULT_CONSEQUENCE_NAME ) ) {
if ("java".equals(consequence.getLanguage())) {
- ctx.getRule().setConsequence( new LambdaConsequence( consequence, enabledTupleOptimization ) );
+ ctx.getRule().setConsequence( new LambdaConsequence( consequence, enabledTupleOptimization, sequenceVarIndexes ) );
} else {
throw new UnsupportedOperationException("Unknown script language for consequence: " + consequence.getLanguage());
}
} else {
- ctx.getRule().addNamedConsequence( name, new LambdaConsequence( consequence, enabledTupleOptimization ) );
+ ctx.getRule().addNamedConsequence( name, new LambdaConsequence( consequence, enabledTupleOptimization, sequenceVarIndexes ) );
+ }
+ }
+
+ /**
+ * Builds a map of variable-name → sequencer-data-index for any consequence variables
+ * that were bound by a sequence step pattern (rather than a normal LHS pattern).
+ * These variables are not accessible via the activation tuple; they must be resolved
+ * from SequencerMemory.getData() at consequence evaluation time.
+ */
+ private Map<String, int[]> buildSequenceVarIndexesForConsequence( RuleContext ctx, Consequence consequence ) {
+ Map<Variable, int[]> seqIndexes = ctx.getSequenceVarIndexes();
+ if (seqIndexes.isEmpty()) {
+ return Map.of();
+ }
+ int totalSeqs = ctx.getSequenceCount();
+ Map<String, int[]> result = null;
+ for (Variable var : consequence.getDeclarations()) {
+ int[] entry = seqIndexes.get(var);
+ if (entry != null) {
+ if (result == null) result = new java.util.HashMap<>();
+ int skip = totalSeqs - 1 - entry[RuleContext.SEQ_VAR_SEQ_IDX];
+ result.put(var.getName(), new int[]{ skip, entry[RuleContext.SEQ_VAR_FILTER_IDX] });
+ }
}
+ return result == null ? Map.of() : result;
}
private Declaration[] getRequiredDeclarationsIfPossible( RuleContext ctx, Consequence consequence, String name ) {
@@ -499,6 +534,27 @@ public class KiePackagesBuilder {
case FORALL: {
return buildForAll( ctx, group, condition );
}
+ case SEQUENCE: {
+ int seqIdx = ctx.nextSeqIndex();
+ SequenceConditionImpl sc = (SequenceConditionImpl) condition;
+ List<Condition> steps = sc.getSubConditions();
+ int n = steps.size();
+ List<Pattern> filters = new ArrayList<>();
+ Step.StepFactory[] stepFactories = new Step.StepFactory[n];
+ int[] gateCounter = new int[]{0};
+
+ for (int i = 0; i < n; i++) {
+ List<LogicGate> stepGates = new ArrayList<>();
+ LogicGate root = buildStepGate(ctx, group, steps.get(i), filters, stepGates, gateCounter, seqIdx);
+ root.setOutput(TerminatingSignalProcessor.get());
+ stepFactories[i] = Step.of(new LogicCircuit(stepGates.toArray(new LogicGate[0])));
+ }
+
+ Sequence seq = new Sequence(0, stepFactories);
+ seq.setFilters(filters.toArray(new Pattern[0]));
+ ctx.getRule().addSequence(seq);
+ return null;
+ }
case CONSEQUENCE:
if (condition instanceof NamedConsequenceImpl) {
NamedConsequenceImpl consequence = (NamedConsequenceImpl) condition;
@@ -549,6 +605,75 @@ public class KiePackagesBuilder {
return transformedForall;
}
+ private static final String DEFERRED_GATE_ERROR =
+ "sequence(...) does not yet support condition type %s. Absence-based " +
+ "gates (nor, nand, xor, xnor, not) and other composites are deferred ";
+
+ private LogicGate buildStepGate(RuleContext ctx, GroupElement group,
+ Condition node, List<Pattern> filters,
+ List<LogicGate> stepGates, int[] gateCounter,
+ int seqIdx) {
+ Condition.Type type = node.getType();
+
+ if (type == Condition.Type.PATTERN) {
+ int idx = filters.size();
+ PatternImpl patternImpl = (PatternImpl) node;
+ RuleConditionElement built = buildPattern(ctx, group, patternImpl);
+ if (!(built instanceof Pattern)) {
+ throw new IllegalStateException(
+ "SEQUENCE leaf must compile to a simple Pattern, got " + built);
+ }
+ filters.add((Pattern) built);
+ // Record variable → (sequence, filter index) so the consequence can look up the matched
+ // fact from SequencerMemory.getData() at rule-fire time.
+ Variable stepVar = patternImpl.getPatternVariable();
+ if (stepVar != null) {
+ ctx.addSequenceVarIndex(stepVar, seqIdx, idx);
+ }
+ LogicGate leaf = new LogicGate(
+ Gates::and,
+ gateCounter[0]++,
+ new int[]{idx},
+ new int[]{idx},
+ 0);
+ stepGates.add(leaf);
+ return leaf;
+ }
+
+ LogicCircuit.LongBiPredicate pred = predicateFor(type);
+ List<Condition> children = node.getSubConditions();
+ if (children.isEmpty()) {
+ throw new IllegalStateException(
+ "SEQUENCE composite node has no children: " + type);
+ }
+ LogicGate[] inputs = new LogicGate[children.size()];
+ for (int i = 0; i < children.size(); i++) {
+ inputs[i] = buildStepGate(ctx, group, children.get(i), filters, stepGates, gateCounter, seqIdx);
+ }
+ LogicGate parent = new LogicGate(
+ pred,
+ gateCounter[0]++,
+ new int[0],
+ new int[0],
+ inputs.length);
+ parent.setInputGates(inputs);
+ for (int k = 0; k < inputs.length; k++) {
+ inputs[k].setOutput(new LogicGateOutputSignalProcessor(SignalIndex.of(parent, k + 1)));
+ }
+ stepGates.add(parent);
+ return parent;
+ }
+
+ private static LogicCircuit.LongBiPredicate predicateFor(Condition.Type t) {
+ switch (t) {
+ case AND: return Gates::and;
+ case OR: return Gates::or;
+ default:
+ throw new UnsupportedOperationException(
+ String.format(DEFERRED_GATE_ERROR, t));
+ }
+ }
+
private RuleConditionElement buildAccumulate( RuleContext ctx, GroupElement group, AccumulatePattern accumulatePattern ) {
Pattern pattern = null;
boolean isGroupBy = accumulatePattern instanceof GroupByPattern;
diff --git a/drools-model/drools-model-compiler/src/main/java/org/drools/modelcompiler/RuleContext.java b/drools-model/drools-model-compiler/src/main/java/org/drools/modelcompiler/RuleContext.java
index a1c826a3994..3bf255f8231 100644
--- a/drools-model/drools-model-compiler/src/main/java/org/drools/modelcompiler/RuleContext.java
+++ b/drools-model/drools-model-compiler/src/main/java/org/drools/modelcompiler/RuleContext.java
@@ -39,6 +39,12 @@ import org.drools.model.Variable;
public class RuleContext {
+ /** Index into the {@code int[]} stored in {@link #sequenceVarIndexes}: the sequence index (raw, before skip conversion). */
+ static final int SEQ_VAR_SEQ_IDX = 0;
+ /** Index into the {@code int[]} stored in {@link #sequenceVarIndexes}: the filter/step index within that sequence. */
+ static final int SEQ_VAR_FILTER_IDX = 1;
+
+
private final KiePackagesBuilder builder;
private final InternalKnowledgePackage pkg;
private final RuleImpl rule;
@@ -48,7 +54,10 @@ public class RuleContext {
private Map<Variable, Declaration> queryDeclarations;
private Map<Variable, Accumulate> accumulateSource;
+ private Map<Variable, int[]> sequenceVarIndexes;
+
private int patternIndex = -1;
+ private int seqIndex = -1;
private boolean needStreamMode = false;
private Deque<Set<Variable>> variablesInOrCondition;
@@ -78,6 +87,14 @@ public class RuleContext {
return ++patternIndex;
}
+ int nextSeqIndex() {
+ return ++seqIndex;
+ }
+
+ int getSequenceCount() {
+ return seqIndex + 1;
+ }
+
void startOrCondition() {
if (variablesInOrCondition == null) {
variablesInOrCondition = new ArrayDeque<>();
@@ -136,6 +153,17 @@ public class RuleContext {
declarations.put( variable, declaration );
}
+ void addSequenceVarIndex( Variable variable, int seqIndex, int filterIndex ) {
+ if (sequenceVarIndexes == null) {
+ sequenceVarIndexes = new HashMap<>();
+ }
+ sequenceVarIndexes.put( variable, new int[]{ seqIndex, filterIndex } );
+ }
+
+ Map<Variable, int[]> getSequenceVarIndexes() {
+ return sequenceVarIndexes == null ? Map.of() : sequenceVarIndexes;
+ }
+
void addGroupByDeclaration( Variable groupKeyVar, Declaration declaration ) {
addDeclaration( groupKeyVar, declaration );
}
diff --git a/drools-model/drools-model-compiler/src/main/java/org/drools/modelcompiler/consequence/LambdaConsequence.java b/drools-model/drools-model-compiler/src/main/java/org/drools/modelcompiler/consequence/LambdaConsequence.java
index 92773c249d2..49921d719da 100644
--- a/drools-model/drools-model-compiler/src/main/java/org/drools/modelcompiler/consequence/LambdaConsequence.java
+++ b/drools-model/drools-model-compiler/src/main/java/org/drools/modelcompiler/consequence/LambdaConsequence.java
@@ -21,11 +21,13 @@ package org.drools.modelcompiler.consequence;
import org.drools.base.base.ValueResolver;
import org.drools.base.definitions.rule.impl.RuleImpl;
import org.drools.base.reteoo.BaseTuple;
+import org.drools.base.reteoo.sequencing.SequencerMemory;
import org.drools.base.rule.Declaration;
import org.drools.base.rule.consequence.Consequence;
import org.drools.core.common.DefaultEventHandle;
import org.drools.core.common.ReteEvaluator;
import org.drools.core.reteoo.RuleTerminalNode;
+import org.drools.core.reteoo.TupleImpl;
import org.drools.core.rule.consequence.KnowledgeHelper;
import org.drools.model.Variable;
import org.kie.api.runtime.rule.FactHandle;
@@ -33,6 +35,7 @@ import org.kie.api.runtime.rule.FactHandle;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
+import java.util.Map;
public class LambdaConsequence implements Consequence<KnowledgeHelper> {
@@ -40,21 +43,34 @@ public class LambdaConsequence implements Consequence<KnowledgeHelper> {
// consequence in linear time by traversing the tuple only once.
private static final boolean ENABLE_LINEARIZED_ARGUMENTS_RETRIEVAL_OPTIMIZATION = true;
+ /** Index into the {@code int[]} values of {@link #sequenceVarIndexes}: tuple-anchor skip count. */
+ private static final int SEQ_ENTRY_SKIP_IDX = 0;
+ /** Index into the {@code int[]} values of {@link #sequenceVarIndexes}: filter/step index within the sequence. */
+ private static final int SEQ_ENTRY_FILTER_IDX = 1;
+
private final org.drools.model.Consequence consequence;
private final int factsNr;
private final boolean enabledTupleOptimization;
private Declaration[] requiredDeclarations;
+ // variable name → [skip, filterIndex] for step-pattern variables.
+ // Non-empty only when the rule contains a sequence() and the consequence references step-bound vars.
+ private final Map<String, int[]> sequenceVarIndexes;
+
private TupleFactSupplier[] factSuppliers;
+ private SequencerFactSupplier[] sequencerFactSuppliers;
private GlobalSupplier[] globalSuppliers;
private Object[] facts;
private FactHandleLookup fhLookup;
- public LambdaConsequence( org.drools.model.Consequence consequence, boolean enabledTupleOptimization) {
+ public LambdaConsequence( org.drools.model.Consequence consequence,
+ boolean enabledTupleOptimization,
+ Map<String, int[]> sequenceVarIndexes) {
this.consequence = consequence;
this.enabledTupleOptimization = ENABLE_LINEARIZED_ARGUMENTS_RETRIEVAL_OPTIMIZATION & enabledTupleOptimization;
this.factsNr = consequence.getVariables().length + ( consequence.isUsingDrools() ? 1 : 0 );
+ this.sequenceVarIndexes = sequenceVarIndexes;
}
@Override
@@ -69,7 +85,7 @@ public class LambdaConsequence implements Consequence<KnowledgeHelper> {
if (enabledTupleOptimization) {
this.requiredDeclarations = declarations;
} else {
- Object[] facts = declarationsToFacts( knowledgeHelper, valueResolver, knowledgeHelper.getTuple(), declarations, consequence.getVariables(), consequence.isUsingDrools() );
+ Object[] facts = declarationsToFacts( knowledgeHelper, valueResolver, knowledgeHelper.getTuple(), declarations, consequence.getVariables(), consequence.isUsingDrools(), sequenceVarIndexes );
consequence.getBlock().execute( facts );
return;
}
@@ -84,6 +100,10 @@ public class LambdaConsequence implements Consequence<KnowledgeHelper> {
}
private static Object[] declarationsToFacts( KnowledgeHelper knowledgeHelper, ValueResolver valueResolver, BaseTuple tuple, Declaration[] declarations, Variable[] vars, boolean useDrools ) {
+ return declarationsToFacts( knowledgeHelper, valueResolver, tuple, declarations, vars, useDrools, Map.of() );
+ }
+
+ private static Object[] declarationsToFacts( KnowledgeHelper knowledgeHelper, ValueResolver valueResolver, BaseTuple tuple, Declaration[] declarations, Variable[] vars, boolean useDrools, Map<String, int[]> sequenceVarIndexes ) {
Object[] objects;
FactHandleLookup fhLookup = useDrools ? new FactHandleLookup.Multi() : null;
@@ -99,12 +119,31 @@ public class LambdaConsequence implements Consequence<KnowledgeHelper> {
int declrCounter = 0;
for (Variable var : vars) {
if ( var.isFact() ) {
- Declaration declaration = declarations[declrCounter++];
- FactHandle fh = getOriginalFactHandle(tuple.get(declaration));
- if ( useDrools ) {
- fhLookup.put( fh.getObject(), fh );
+ int[] seqEntry = sequenceVarIndexes.get( var.getName() );
+ if ( seqEntry != null ) {
+ // Sequence-step variable: not in the activation tuple — resolve from SequencerMemory.
+ BaseTuple anchor = findSequencerAnchor( tuple, seqEntry[SEQ_ENTRY_SKIP_IDX] );
+ if ( anchor == null ) {
+ throw new IllegalStateException(
+ "declarationsToFacts: no SequencerMemory anchor found at skip=" + seqEntry[SEQ_ENTRY_SKIP_IDX] +
+ " for variable '" + var.getName() + "'. " +
+ "This indicates a compile-time skip-count miscalculation in KiePackagesBuilder." );
+ }
+ SequencerMemory sequencerMemory = (SequencerMemory) anchor.getContextObject();
+ FactHandle fh = getOriginalFactHandle( (FactHandle) sequencerMemory.getData().get( seqEntry[SEQ_ENTRY_FILTER_IDX] ) );
+ if ( useDrools && fhLookup != null ) {
+ fhLookup.put( fh.getObject(), fh );
+ }
+ objects[index++] = fh.getObject();
+ declrCounter++; // consume the declaration slot (which is null for sequence vars)
+ } else {
+ Declaration declaration = declarations[declrCounter++];
+ FactHandle fh = getOriginalFactHandle(tuple.get(declaration));
+ if ( useDrools ) {
+ fhLookup.put( fh.getObject(), fh );
+ }
+ objects[index++] = declaration.getValue( valueResolver, fh );
}
- objects[index++] = declaration.getValue( valueResolver, fh );
} else {
objects[index++] = valueResolver.getGlobal( var.getName() );
}
@@ -112,6 +151,28 @@ public class LambdaConsequence implements Consequence<KnowledgeHelper> {
return objects;
}
+ /**
+ * Walks up the tuple parent chain counting {@link SequencerMemory} anchors.
+ * Returns the {@code skip}-th anchor found (0 = innermost/closest to terminal).
+ *
+ * Must use {@link TupleImpl#getLeftParent()} rather than {@link BaseTuple#getParent()}:
+ * in subnetwork tuples the two fields diverge — getParent() skips the SequencerMemory
+ * anchor tuples that getLeftParent() traverses.
+ */
+ private static TupleImpl findSequencerAnchor(BaseTuple tuple, int skip) {
+ TupleImpl t = ((TupleImpl) tuple).getLeftParent();
+ while (t != null) {
+ if (t.getContextObject() instanceof SequencerMemory) {
+ if (skip == 0) {
+ return t;
+ }
+ skip--;
+ }
+ t = t.getLeftParent();
+ }
+ return null;
+ }
+
private static FactHandle getOriginalFactHandle( FactHandle handle ) {
if ( !handle.isEvent() ) {
return handle;
@@ -148,6 +209,12 @@ public class LambdaConsequence implements Consequence<KnowledgeHelper> {
tuple = factSuppliers[j].resolveAndStore(facts, reteEvaluator, tuple, fhLookup);
}
+ if (sequencerFactSuppliers != null) {
+ for (int j = 0; j < sequencerFactSuppliers.length; j++) {
+ sequencerFactSuppliers[j].resolveAndStore(facts, reteEvaluator, knowledgeHelper.getTuple(), fhLookup);
+ }
+ }
+
if (globalSuppliers != null) {
for (int j = 0; j < globalSuppliers.length; j++) {
globalSuppliers[j].resolveAndStore(facts, reteEvaluator);
@@ -164,7 +231,8 @@ public class LambdaConsequence implements Consequence<KnowledgeHelper> {
}
BaseTuple tuple = knowledgeHelper.getTuple();
- List<TupleFactSupplier> factSuppliers = new ArrayList<>();
+ List<TupleFactSupplier> tupleFactSuppliers = new ArrayList<>();
+ List<SequencerFactSupplier> seqFactSuppliers = new ArrayList<>();
List<GlobalSupplier> globalSuppliers = new ArrayList<>();
Object[] facts;
@@ -180,7 +248,14 @@ public class LambdaConsequence implements Consequence<KnowledgeHelper> {
int declrCounter = 0;
for (Variable var : vars) {
if ( var.isFact() ) {
- factSuppliers.add( new TupleFactSupplier( supplierIndex, requiredDeclarations[declrCounter++], consequence.isUsingDrools() ) );
+ int[] entry = sequenceVarIndexes.get(var.getName());
+ if (entry != null) {
+ // Step-pattern variable: resolved from SequencerMemory, not from the tuple.
+ seqFactSuppliers.add( new SequencerFactSupplier( supplierIndex, entry[SEQ_ENTRY_SKIP_IDX], entry[SEQ_ENTRY_FILTER_IDX], consequence.isUsingDrools() ) );
+ declrCounter++; // still consume the declaration slot
+ } else {
+ tupleFactSuppliers.add( new TupleFactSupplier( supplierIndex, requiredDeclarations[declrCounter++], consequence.isUsingDrools() ) );
+ }
} else {
facts[supplierIndex] = reteEvaluator.getGlobal( var.getName() );
globalSuppliers.add( new GlobalSupplier( supplierIndex, var.getName() ) );
@@ -190,16 +265,16 @@ public class LambdaConsequence implements Consequence<KnowledgeHelper> {
FactHandleLookup fhLookup = null;
if ( consequence.isUsingDrools() ) {
- fhLookup = FactHandleLookup.create( factSuppliers.size() );
+ fhLookup = FactHandleLookup.create( tupleFactSuppliers.size() + seqFactSuppliers.size() );
facts[0] = new DroolsImpl( knowledgeHelper, reteEvaluator, fhLookup );
}
- Collections.sort( factSuppliers );
+ Collections.sort( tupleFactSuppliers );
Collections.sort( globalSuppliers );
BaseTuple current = tuple;
boolean first = true;
- for (TupleFactSupplier tupleFactSupplier : factSuppliers) {
+ for (TupleFactSupplier tupleFactSupplier : tupleFactSuppliers) {
int targetTupleIndex = tupleFactSupplier.declarationTupleIndex;
tupleFactSupplier.offsetFromPrior = 0;
@@ -214,9 +289,15 @@ public class LambdaConsequence implements Consequence<KnowledgeHelper> {
tupleFactSupplier.resolveAndStore(facts, reteEvaluator, current.getFactHandle(), fhLookup);
}
+ // Resolve sequence-step variables immediately (they are always stable once the sequence fired).
+ for (SequencerFactSupplier seqSupplier : seqFactSuppliers) {
+ seqSupplier.resolveAndStore(facts, reteEvaluator, tuple, fhLookup);
+ }
+
// factSuppliers has to be last because factSuppliers is used as an initialization flag in fetchFacts(). See DROOLS-6961
this.globalSuppliers = globalSuppliers.isEmpty() ? null : globalSuppliers.toArray( new GlobalSupplier[globalSuppliers.size()] );
- this.factSuppliers = factSuppliers.toArray( new TupleFactSupplier[factSuppliers.size()] );
+ this.sequencerFactSuppliers = seqFactSuppliers.isEmpty() ? null : seqFactSuppliers.toArray( new SequencerFactSupplier[seqFactSuppliers.size()] );
+ this.factSuppliers = tupleFactSuppliers.toArray( new TupleFactSupplier[tupleFactSuppliers.size()] );
if (!reteEvaluator.getRuleSessionConfiguration().isThreadSafe()) {
this.facts = facts;
@@ -290,4 +371,43 @@ public class LambdaConsequence implements Consequence<KnowledgeHelper> {
return o.declarationTupleIndex - declarationTupleIndex;
}
}
+
+ /**
+ * Resolves a fact that was matched by a sequence step pattern.
+ * Instead of reading from the activation tuple (where step-matched facts do not appear),
+ * it reads from SequencerMemory.getData() on the anchor left-tuple's context object.
+ *
+ * The activation tuple chain is: child (fired) → anchor (SequencerMemory as context).
+ * SequencerMemory.getData() is a CircularArrayList indexed by filter/step index.
+ */
+ private static class SequencerFactSupplier {
+ private final int supplierIndex;
+ private final int skip;
+ private final int filterIndex;
+ private boolean useDrools;
+
+ private SequencerFactSupplier( int supplierIndex, int skip, int filterIndex, boolean useDrools ) {
+ this.supplierIndex = supplierIndex;
+ this.skip = skip;
+ this.filterIndex = filterIndex;
+ this.useDrools = useDrools;
+ }
+
+ public void resolveAndStore(Object[] facts, ValueResolver reteEvaluator, BaseTuple tuple, FactHandleLookup fhLookup) {
+ // Walk up to the anchor tuple — counting past `skip` SequencerMemory anchors.
+ BaseTuple anchor = findSequencerAnchor(tuple, skip);
+ if (anchor == null) {
+ throw new IllegalStateException(
+ "SequencerFactSupplier: no SequencerMemory anchor found at skip=" + skip +
+ " (supplierIndex=" + supplierIndex + ", filterIndex=" + filterIndex +
+ "). This indicates a compile-time skip-count miscalculation in KiePackagesBuilder.");
+ }
+ SequencerMemory sequencerMemory = (SequencerMemory) anchor.getContextObject();
+ FactHandle fh = getOriginalFactHandle( (FactHandle) sequencerMemory.getData().get(filterIndex) );
+ if ( useDrools && fhLookup != null ) {
+ fhLookup.put( fh.getObject(), fh );
+ }
+ facts[supplierIndex] = fh.getObject();
+ }
+ }
}
\ No newline at end of file
diff --git a/drools-model/drools-model-compiler/src/test/java/org/drools/modelcompiler/PatternDSLSequenceAdvancedTest.java b/drools-model/drools-model-compiler/src/test/java/org/drools/modelcompiler/PatternDSLSequenceAdvancedTest.java
new file mode 100644
index 00000000000..6714f3a3c3f
--- /dev/null
+++ b/drools-model/drools-model-compiler/src/test/java/org/drools/modelcompiler/PatternDSLSequenceAdvancedTest.java
@@ -0,0 +1,518 @@
+/*
+ * 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.modelcompiler;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.drools.model.Rule;
+import org.drools.model.Variable;
+import org.drools.model.impl.ModelImpl;
+import org.drools.modelcompiler.domain.Person;
+import org.drools.modelcompiler.domain.Relationship;
+import org.drools.modelcompiler.domain.SensorEvents.AlarmRaised;
+import org.drools.modelcompiler.domain.SensorEvents.CalibrationPassed;
+import org.drools.modelcompiler.domain.SensorEvents.HeartbeatOk;
+import org.drools.modelcompiler.domain.SensorEvents.MaintenanceScheduled;
+import org.drools.modelcompiler.domain.SensorEvents.MonitoringStation;
+import org.drools.modelcompiler.domain.SensorEvents.OperatorAcknowledged;
+import org.drools.modelcompiler.domain.SensorEvents.SensorActivated;
+import org.drools.modelcompiler.domain.Toy;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+import org.kie.api.runtime.KieSession;
+import org.kie.api.runtime.rule.FactHandle;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.drools.model.DSL.declarationOf;
+import static org.drools.model.DSL.execute;
+import static org.drools.model.PatternDSL.and;
+import static org.drools.model.PatternDSL.or;
+import static org.drools.model.PatternDSL.pattern;
+import static org.drools.model.PatternDSL.rule;
+import static org.drools.model.PatternDSL.sequence;
+
+public class PatternDSLSequenceAdvancedTest {
+
+ private final List<String> results = new ArrayList<>();
+ private KieSession ksession;
+
+ @AfterEach
+ public void tearDown() {
+ results.clear();
+ if (ksession != null) {
+ ksession.dispose();
+ ksession = null;
+ }
+ }
+
+ private void insertAndFire(Object... facts) {
+ for (Object f : facts) { ksession.insert(f); }
+ ksession.fireAllRules();
+ }
+
+ @Test
+ public void noiseEventsBetweenStepsAreIgnored() {
+ Variable<Person> personV = declarationOf(Person.class);
+ Variable<Toy> toyV = declarationOf(Toy.class);
+ Variable<Relationship> relV = declarationOf(Relationship.class);
+
+ Rule rule = rule("noise-rule").build(
+ pattern(personV),
+ sequence(
+ pattern(toyV).expr("is-ball", t -> t.getName().equals("ball")),
+ pattern(relV).expr("is-go", r -> r.getStart().equals("go"))
+ ),
+ execute(() -> results.add("fired"))
+ );
+
+ ksession = KieBaseBuilder.createKieBaseFromModel(new ModelImpl().addRule(rule)).newKieSession();
+
+ insertAndFire(new Person("anchor"));
+
+ insertAndFire(new Toy("ball")); // step 1 satisfied
+
+ // Noise: wrong toy name (must not re-satisfy step 1 or advance step 2)
+ insertAndFire(new Toy("other"));
+ assertThat(results).isEmpty();
+
+ // Noise: wrong relationship start (must not fire rule)
+ insertAndFire(new Relationship("nope", "x"));
+ assertThat(results).isEmpty();
+
+ // Correct step 2
+ insertAndFire(new Relationship("go", "done"));
+ assertThat(results).containsExactly("fired");
+ }
+
+ @Test
+ public void eventBeforeAnchorIsNotCaptured() {
+ Variable<Person> personV = declarationOf(Person.class);
+ Variable<Toy> toyV = declarationOf(Toy.class);
+ Variable<Relationship> relV = declarationOf(Relationship.class);
+
+ Rule rule = rule("pre-anchor-rule").build(
+ pattern(personV),
+ sequence(
+ pattern(toyV).expr("is-ball", t -> t.getName().equals("ball")),
+ pattern(relV).expr("is-go", r -> r.getStart().equals("go"))
+ ),
+ execute(() -> results.add("fired"))
+ );
+
+ ksession = KieBaseBuilder.createKieBaseFromModel(new ModelImpl().addRule(rule)).newKieSession();
+
+ // Step-1 event arrives BEFORE the anchor
+ insertAndFire(new Toy("ball"));
+
+ // Anchor inserted — sequencer starts, but the ball is already in the past
+ insertAndFire(new Person("anchor"));
+
+ // Step-2 event: the sequencer is at step 0 (ball not yet seen), must NOT fire
+ insertAndFire(new Relationship("go", "done"));
+
+ assertThat(results).isEmpty();
+ }
+
+ @Test
+ public void orSecondBranchAloneFires() {
+ Variable<MonitoringStation> stationV = declarationOf(MonitoringStation.class);
+ Variable<SensorActivated> activatedV = declarationOf(SensorActivated.class);
+ Variable<HeartbeatOk> heartbeatV = declarationOf(HeartbeatOk.class);
+ Variable<AlarmRaised> alarmV = declarationOf(AlarmRaised.class);
+ Variable<OperatorAcknowledged> ackV = declarationOf(OperatorAcknowledged.class);
+
+ Rule rule = rule("or-second-branch").build(
+ pattern(stationV),
+ sequence(
+ pattern(activatedV).expr("s1", a -> a.getSensorId().equals("s1")),
+ or(
+ pattern(heartbeatV).expr("hb", h -> h.getSensorId().equals("s1")),
+ pattern(alarmV).expr("al", al -> al.getSeverity().equals("high"))
+ ),
+ pattern(ackV).expr("ack", a -> a.getOperator().equals("alice"))
+ ),
+ execute(() -> results.add("fired"))
+ );
+
+ ksession = KieBaseBuilder.createKieBaseFromModel(new ModelImpl().addRule(rule)).newKieSession();
+
+ insertAndFire(new MonitoringStation("station-1"));
+ insertAndFire(new SensorActivated("s1"));
+
+ // Only the alarm branch — no heartbeat
+ insertAndFire(new AlarmRaised("s1", "high"));
+ insertAndFire(new OperatorAcknowledged("s1", "alice"));
+
+ assertThat(results).containsExactly("fired");
+ }
+
+ @Test
+ public void orBothBranchesArriveRuleFiresOnce() {
+ Variable<MonitoringStation> stationV = declarationOf(MonitoringStation.class);
+ Variable<SensorActivated> activatedV = declarationOf(SensorActivated.class);
+ Variable<HeartbeatOk> heartbeatV = declarationOf(HeartbeatOk.class);
+ Variable<AlarmRaised> alarmV = declarationOf(AlarmRaised.class);
+ Variable<OperatorAcknowledged> ackV = declarationOf(OperatorAcknowledged.class);
+
+ Rule rule = rule("or-both-branches").build(
+ pattern(stationV),
+ sequence(
+ pattern(activatedV).expr("s1", a -> a.getSensorId().equals("s1")),
+ or(
+ pattern(heartbeatV).expr("hb", h -> h.getSensorId().equals("s1")),
+ pattern(alarmV).expr("al", al -> al.getSeverity().equals("high"))
+ ),
+ pattern(ackV).expr("ack", a -> a.getOperator().equals("alice"))
+ ),
+ execute(() -> results.add("fired"))
+ );
+
+ ksession = KieBaseBuilder.createKieBaseFromModel(new ModelImpl().addRule(rule)).newKieSession();
+
+ insertAndFire(new MonitoringStation("station-1"));
+ insertAndFire(new SensorActivated("s1"));
+
+ // First branch fires — OR step advances, gate deactivates
+ insertAndFire(new HeartbeatOk("s1"));
+
+ // Second branch arrives AFTER step has advanced — must be ignored
+ insertAndFire(new AlarmRaised("s1", "high"));
+
+ insertAndFire(new OperatorAcknowledged("s1", "alice"));
+
+ // Rule must have fired exactly once
+ assertThat(results).containsExactly("fired");
+ }
+
+ @Test
+ public void andGateWithInterleavedNoiseWaitsForBothSignals() {
+ Variable<MonitoringStation> stationV = declarationOf(MonitoringStation.class);
+ Variable<SensorActivated> activatedV = declarationOf(SensorActivated.class);
+ Variable<HeartbeatOk> heartbeatV = declarationOf(HeartbeatOk.class);
+ Variable<AlarmRaised> alarmV = declarationOf(AlarmRaised.class);
+ Variable<OperatorAcknowledged> ackV = declarationOf(OperatorAcknowledged.class);
+
+ Rule rule = rule("and-noise-rule").build(
+ pattern(stationV),
+ sequence(
+ pattern(activatedV).expr("s1", a -> a.getSensorId().equals("s1")),
+ and(
+ pattern(heartbeatV).expr("hb", h -> h.getSensorId().equals("s1")),
+ pattern(alarmV).expr("al", al -> al.getSeverity().equals("high"))
+ ),
+ pattern(ackV).expr("ack", a -> a.getOperator().equals("alice"))
+ ),
+ execute(() -> results.add("fired"))
+ );
+
+ ksession = KieBaseBuilder.createKieBaseFromModel(new ModelImpl().addRule(rule)).newKieSession();
+
+ insertAndFire(new MonitoringStation("station-1"));
+ insertAndFire(new SensorActivated("s1"));
+
+ // AND child #1 (heartbeat for s1)
+ insertAndFire(new HeartbeatOk("s1"));
+ assertThat(results).isEmpty(); // still waiting for alarm
+
+ // Noise: heartbeat for a different sensor — must not count as s1's signal
+ insertAndFire(new HeartbeatOk("s2"));
+ assertThat(results).isEmpty();
+
+ // Noise: low-severity alarm — doesn't satisfy the "high" constraint
+ insertAndFire(new AlarmRaised("s1", "low"));
+ assertThat(results).isEmpty();
+
+ // AND child #2 (high alarm for s1) — both gates now satisfied
+ insertAndFire(new AlarmRaised("s1", "high"));
+
+ insertAndFire(new OperatorAcknowledged("s1", "alice"));
+ assertThat(results).containsExactly("fired");
+ }
+
+ @Test
+ public void threeStepSequenceFiresInOrder() {
+ Variable<Person> personV = declarationOf(Person.class);
+ Variable<Toy> toyV = declarationOf(Toy.class);
+ Variable<Relationship> relV = declarationOf(Relationship.class);
+ Variable<SensorActivated> sensorV = declarationOf(SensorActivated.class);
+
+ Rule rule = rule("three-step-rule").build(
+ pattern(personV),
+ sequence(
+ pattern(toyV).expr("is-ball", t -> t.getName().equals("ball")),
+ pattern(relV).expr("is-go", r -> r.getStart().equals("go")),
+ pattern(sensorV).expr("is-s1", s -> s.getSensorId().equals("s1"))
+ ),
+ execute(() -> results.add("fired"))
+ );
+
+ ksession = KieBaseBuilder.createKieBaseFromModel(new ModelImpl().addRule(rule)).newKieSession();
+
+ insertAndFire(new Person("anchor"));
+
+ insertAndFire(new Toy("ball"));
+ assertThat(results).isEmpty();
+
+ insertAndFire(new Relationship("go", "done"));
+ assertThat(results).isEmpty();
+
+ insertAndFire(new SensorActivated("s1"));
+ assertThat(results).containsExactly("fired");
+ }
+
+ @Test
+ public void threeStepSequenceOutOfOrderDoesNotFire() {
+ Variable<Person> personV = declarationOf(Person.class);
+ Variable<Toy> toyV = declarationOf(Toy.class);
+ Variable<Relationship> relV = declarationOf(Relationship.class);
+ Variable<SensorActivated> sensorV = declarationOf(SensorActivated.class);
+
+ Rule rule = rule("three-step-oo-rule").build(
+ pattern(personV),
+ sequence(
+ pattern(toyV).expr("is-ball", t -> t.getName().equals("ball")),
+ pattern(relV).expr("is-go", r -> r.getStart().equals("go")),
+ pattern(sensorV).expr("is-s1", s -> s.getSensorId().equals("s1"))
+ ),
+ execute(() -> results.add("fired"))
+ );
+
+ ksession = KieBaseBuilder.createKieBaseFromModel(new ModelImpl().addRule(rule)).newKieSession();
+
+ insertAndFire(new Person("anchor"));
+
+ insertAndFire(new Toy("ball")); // step 1 done
+
+ // Step 3 event arrives before step 2 — must not skip step 2
+ insertAndFire(new SensorActivated("s1"));
+ assertThat(results).isEmpty();
+
+ // Step 2 now — but step 3 event in the past, so still at step 2 waiting for step 3 again
+ insertAndFire(new Relationship("go", "done")); // step 2 done
+ assertThat(results).isEmpty();
+
+ // Step 3 again — now fires
+ insertAndFire(new SensorActivated("s1"));
+ assertThat(results).containsExactly("fired");
+ }
+
+ @Test
+ public void retractAnchorAfterCompletionIsNoOp() {
+ Variable<Person> personV = declarationOf(Person.class);
+ Variable<Toy> toyV = declarationOf(Toy.class);
+ Variable<Relationship> relV = declarationOf(Relationship.class);
+
+ Rule rule = rule("retract-after-rule").build(
+ pattern(personV),
+ sequence(
+ pattern(toyV).expr("is-ball", t -> t.getName().equals("ball")),
+ pattern(relV).expr("is-go", r -> r.getStart().equals("go"))
+ ),
+ execute(() -> results.add("fired"))
+ );
+
+ ksession = KieBaseBuilder.createKieBaseFromModel(new ModelImpl().addRule(rule)).newKieSession();
+
+ FactHandle anchorHandle = ksession.insert(new Person("anchor"));
+ ksession.fireAllRules();
+
+ insertAndFire(new Toy("ball"));
+ insertAndFire(new Relationship("go", "done")); // sequence complete
+ assertThat(results).containsExactly("fired");
+
+ // Retract after completion — must not throw, must not fire again
+ ksession.retract(anchorHandle);
+ ksession.fireAllRules();
+
+ assertThat(results).hasSize(1);
+ }
+
+ @Test
+ public void firstAnchorCompletesWhileSecondIsMidSequence() {
+ Variable<Person> personV = declarationOf(Person.class);
+ Variable<Toy> toyV = declarationOf(Toy.class);
+ Variable<Relationship> relV = declarationOf(Relationship.class);
+
+ Rule rule = rule("concurrent-anchor-rule").build(
+ pattern(personV),
+ sequence(
+ pattern(toyV).expr("is-ball", t -> t.getName().equals("ball")),
+ pattern(relV).expr("is-go", r -> r.getStart().equals("go"))
+ ),
+ execute(() -> results.add("fired"))
+ );
+
+ ksession = KieBaseBuilder.createKieBaseFromModel(new ModelImpl().addRule(rule)).newKieSession();
+
+ FactHandle anchorAHandle = ksession.insert(new Person("anchor-A"));
+ FactHandle anchorBHandle = ksession.insert(new Person("anchor-B"));
+ ksession.fireAllRules(); // both sequencers start
+
+ // Both anchors consume step 1
+ insertAndFire(new Toy("ball"));
+
+ // Step 2 arrives — both sequences should be able to complete
+ insertAndFire(new Relationship("go", "done"));
+
+ // TODO: once per-tuple DynamicFilter isolation is fixed, change to hasSize(2).
+ // Current known limitation: only one sequencer advances due to shared filter slots.
+ assertThat(results).hasSizeGreaterThanOrEqualTo(1);
+ }
+
+ @Test
+ public void sameTypeInFirstAndThirdStep() {
+ Variable<Person> personV = declarationOf(Person.class);
+ Variable<Toy> toyV1 = declarationOf(Toy.class);
+ Variable<Relationship> relV = declarationOf(Relationship.class);
+ Variable<Toy> toyV2 = declarationOf(Toy.class);
+
+ Rule rule = rule("type-reuse-rule").build(
+ pattern(personV),
+ sequence(
+ pattern(toyV1).expr("is-ball", t -> t.getName().equals("ball")),
+ pattern(relV).expr("is-go", r -> r.getStart().equals("go")),
+ pattern(toyV2).expr("is-doll", t -> t.getName().equals("doll"))
+ ),
+ execute(() -> results.add("fired"))
+ );
+
+ ksession = KieBaseBuilder.createKieBaseFromModel(new ModelImpl().addRule(rule)).newKieSession();
+
+ insertAndFire(new Person("anchor"));
+
+ // Step 1: Toy("ball")
+ insertAndFire(new Toy("ball"));
+ assertThat(results).isEmpty();
+
+ // Wrong Toy during step 2 wait — must not advance step 3
+ insertAndFire(new Toy("doll"));
+ assertThat(results).isEmpty();
+
+ // Step 2: Relationship("go")
+ insertAndFire(new Relationship("go", "done"));
+ assertThat(results).isEmpty();
+
+ // Step 3: Toy("doll") — same type as step 1, different filter
+ insertAndFire(new Toy("doll"));
+ assertThat(results).containsExactly("fired");
+ }
+
+ @Test
+ public void secondSequenceEventBeforeFirstSequenceCompletesIsIgnored() {
+ Variable<Person> personV = declarationOf(Person.class);
+ Variable<Toy> toyV = declarationOf(Toy.class);
+ Variable<Relationship> relV = declarationOf(Relationship.class);
+
+ Rule rule = rule("chained-seq-order-rule").build(
+ pattern(personV),
+ sequence(
+ pattern(toyV).expr("is-ball", t -> t.getName().equals("ball"))
+ ),
+ sequence(
+ pattern(relV).expr("is-go", r -> r.getStart().equals("go"))
+ ),
+ execute(() -> results.add("fired"))
+ );
+
+ ksession = KieBaseBuilder.createKieBaseFromModel(new ModelImpl().addRule(rule)).newKieSession();
+
+ insertAndFire(new Person("anchor"));
+
+ // Relationship arrives before Toy — second SequenceNode not yet active
+ insertAndFire(new Relationship("go", "done"));
+ assertThat(results).isEmpty();
+
+ // Now Toy arrives — first sequence completes, second SequenceNode activates
+ insertAndFire(new Toy("ball"));
+ assertThat(results).isEmpty(); // second step not yet seen
+
+ // Relationship again — now the second SequenceNode is listening
+ insertAndFire(new Relationship("go", "done"));
+ assertThat(results).containsExactly("fired");
+ }
+
+ @Test
+ public void batchInsertBeforeFireDoesNotFire() {
+ Variable<Person> personV = declarationOf(Person.class);
+ Variable<Toy> toyV = declarationOf(Toy.class);
+ Variable<Relationship> relV = declarationOf(Relationship.class);
+
+ Rule rule = rule("batch-insert-rule").build(
+ pattern(personV),
+ sequence(
+ pattern(toyV).expr("is-ball", t -> t.getName().equals("ball")),
+ pattern(relV).expr("is-go", r -> r.getStart().equals("go"))
+ ),
+ execute(() -> results.add("fired"))
+ );
+
+ ksession = KieBaseBuilder.createKieBaseFromModel(new ModelImpl().addRule(rule)).newKieSession();
+
+ // Insert all facts before any fireAllRules() — step signals are lost
+ // because the sequencer hasn't started yet
+ ksession.insert(new Person("anchor"));
+ ksession.insert(new Toy("ball"));
+ ksession.insert(new Relationship("go", "done"));
+
+ ksession.fireAllRules();
+
+ // Sequence does not fire: step facts were inserted before the sequencer
+ // started. This is a known limitation of the current AlphaAdapter design.
+ assertThat(results).isEmpty();
+ }
+
+ @Test
+ public void orGateWithFiveInputsFiresOnAnyBranch() {
+
+ final Variable<MonitoringStation> stationV = declarationOf(MonitoringStation.class);
+ final Variable<SensorActivated> activatedV = declarationOf(SensorActivated.class);
+ final Variable<HeartbeatOk> heartbeatV = declarationOf(HeartbeatOk.class);
+ final Variable<AlarmRaised> alarmV = declarationOf(AlarmRaised.class);
+ final Variable<CalibrationPassed> calibV = declarationOf(CalibrationPassed.class);
+ final Variable<OperatorAcknowledged> ackV = declarationOf(OperatorAcknowledged.class);
+ final Variable<MaintenanceScheduled> maintenanceV = declarationOf(MaintenanceScheduled.class);
+
+ Rule fiveInputRule =
+ rule("five-input-or").build(
+ pattern(stationV),
+ sequence(
+ pattern(activatedV).expr("anchor", a -> a.getSensorId().equals("sensor-1")),
+ or(
+ pattern(heartbeatV).expr("hb", h -> h.getSensorId().equals("sensor-1")),
+ pattern(alarmV).expr("alarm", al -> al.getSeverity().equals("high")),
+ pattern(calibV).expr("calib", c -> c.getSensorId().equals("sensor-1")),
+ pattern(ackV).expr("ack", a -> a.getOperator().equals("alice")),
+ pattern(maintenanceV).expr("maint", m -> m.getSensorId().equals("sensor-1"))
+ ),
+ pattern(activatedV).expr("final", a -> a.getSensorId().equals("sensor-2"))
+ ),
+ execute(() -> results.add("done"))
+ );
+
+ ksession = KieBaseBuilder.createKieBaseFromModel(new ModelImpl().addRule(fiveInputRule)).newKieSession();
+
+ insertAndFire(new MonitoringStation("station-1"));
+ insertAndFire(new SensorActivated("sensor-1")); // anchor fires; OR step activates
+ insertAndFire(new MaintenanceScheduled("sensor-1")); // 5th OR branch triggers default loop path
+ insertAndFire(new SensorActivated("sensor-2")); // final step; consequence fires
+
+ assertThat(results).containsExactly("done");
+ }
+}
diff --git a/drools-model/drools-model-compiler/src/test/java/org/drools/modelcompiler/PatternDSLSequenceCompositeTest.java b/drools-model/drools-model-compiler/src/test/java/org/drools/modelcompiler/PatternDSLSequenceCompositeTest.java
new file mode 100644
index 00000000000..93f4e18f843
--- /dev/null
+++ b/drools-model/drools-model-compiler/src/test/java/org/drools/modelcompiler/PatternDSLSequenceCompositeTest.java
@@ -0,0 +1,335 @@
+/*
+ * 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.modelcompiler;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.drools.model.Rule;
+import org.drools.model.Variable;
+import org.drools.model.impl.ModelImpl;
+import org.drools.modelcompiler.domain.SensorEvents.AlarmRaised;
+import org.drools.modelcompiler.domain.SensorEvents.CalibrationPassed;
+import org.drools.modelcompiler.domain.SensorEvents.HeartbeatOk;
+import org.drools.modelcompiler.domain.SensorEvents.MonitoringStation;
+import org.drools.modelcompiler.domain.SensorEvents.OperatorAcknowledged;
+import org.drools.modelcompiler.domain.SensorEvents.SensorActivated;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+import org.kie.api.runtime.KieSession;
+import org.kie.api.runtime.rule.FactHandle;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.drools.model.DSL.declarationOf;
+import static org.drools.model.DSL.execute;
+import static org.drools.model.PatternDSL.and;
+import static org.drools.model.PatternDSL.or;
+import static org.drools.model.PatternDSL.pattern;
+import static org.drools.model.PatternDSL.rule;
+import static org.drools.model.PatternDSL.sequence;
+
+public class PatternDSLSequenceCompositeTest {
+
+ private final Variable<MonitoringStation> station = declarationOf(MonitoringStation.class);
+ private final Variable<SensorActivated> sensorActivated = declarationOf(SensorActivated.class);
+ private final Variable<HeartbeatOk> heartbeat = declarationOf(HeartbeatOk.class);
+ private final Variable<AlarmRaised> alarm = declarationOf(AlarmRaised.class);
+ private final Variable<CalibrationPassed> calibration = declarationOf(CalibrationPassed.class);
+ private final Variable<OperatorAcknowledged> ack = declarationOf(OperatorAcknowledged.class);
+
+ private KieSession ksession;
+
+ @AfterEach
+ public void tearDown() {
+ if (ksession != null) {
+ ksession.dispose();
+ }
+ }
+
+ private void insertAndFire(Object... facts) {
+ for (Object fact : facts) {
+ ksession.insert(fact);
+ }
+ ksession.fireAllRules();
+ }
+
+ @Test
+
+ public void sequenceFiresWithOrOfTwo() {
+ // Step 1 is or(heartbeat, alarm). Either signal is enough to advance.
+ final List<String> results = new ArrayList<>();
+
+ Rule orRule =
+ rule("or-rule").build(
+ pattern(station),
+ sequence(
+ pattern(sensorActivated).expr("anchor", a -> a.getSensorId().equals("sensor-1")),
+ or(
+ pattern(heartbeat).expr("ok", h -> h.getSensorId().equals("sensor-1")),
+ pattern(alarm).expr("hi", al -> al.getSeverity().equals("high"))
+ ),
+ pattern(ack).expr("ack", a -> a.getOperator().equals("alice"))
+ ),
+ execute(() -> results.add("acknowledged"))
+ );
+
+ ksession = KieBaseBuilder.createKieBaseFromModel(new ModelImpl().addRule(orRule)).newKieSession();
+
+ insertAndFire(new MonitoringStation("station-1"));
+ insertAndFire(new SensorActivated("sensor-1"));
+ insertAndFire(new HeartbeatOk("sensor-1")); // OR child #1 fires → step advances
+ insertAndFire(new OperatorAcknowledged("sensor-1", "alice"));
+
+ assertThat(results).containsExactly("acknowledged");
+ }
+
+ @Test
+ public void sequenceFiresWithAndOfTwo() {
+ // Step 1 is and(heartbeat, alarm). Both signals must arrive before step advances.
+ final List<String> results = new ArrayList<>();
+
+ Rule andRule =
+ rule("and-rule").build(
+ pattern(station),
+ sequence(
+ pattern(sensorActivated).expr("anchor", a -> a.getSensorId().equals("sensor-1")),
+ and(
+ pattern(heartbeat).expr("ok", h -> h.getSensorId().equals("sensor-1")),
+ pattern(alarm).expr("hi", al -> al.getSeverity().equals("high"))
+ ),
+ pattern(ack).expr("ack", a -> a.getOperator().equals("alice"))
+ ),
+ execute(() -> results.add("acknowledged"))
+ );
+
+ ksession = KieBaseBuilder.createKieBaseFromModel(new ModelImpl().addRule(andRule)).newKieSession();
+
+ insertAndFire(new MonitoringStation("station-1"));
+ insertAndFire(new SensorActivated("sensor-1"));
+ insertAndFire(new HeartbeatOk("sensor-1")); // AND child #1 fires → step still waiting
+ insertAndFire(new AlarmRaised("sensor-1", "high")); // AND child #2 fires → step advances
+ insertAndFire(new OperatorAcknowledged("sensor-1", "alice"));
+
+ assertThat(results).containsExactly("acknowledged");
+ }
+
+ @Test
+ public void sequenceFiresWithNestedAndInsideOr() {
+ // Step 1 is or(heartbeat, and(calibration, alarm)).
+ // Either branch suffices: a heartbeat alone, or calibration + alarm together.
+ final List<String> results = new ArrayList<>();
+
+ Rule nestedRule =
+ rule("nested-rule").build(
+ pattern(station),
+ sequence(
+ pattern(sensorActivated).expr("anchor", a -> a.getSensorId().equals("sensor-1")),
+ or(
+ pattern(heartbeat).expr("ok", h -> h.getSensorId().equals("sensor-1")),
+ and(
+ pattern(calibration).expr("calibrated", c -> c.getSensorId().equals("sensor-1")),
+ pattern(alarm).expr("hi", al -> al.getSeverity().equals("high"))
+ )
+ ),
+ pattern(ack).expr("ack", a -> a.getOperator().equals("alice"))
+ ),
+ execute(() -> results.add("acknowledged"))
+ );
+
+ ksession = KieBaseBuilder.createKieBaseFromModel(new ModelImpl().addRule(nestedRule)).newKieSession();
+
+ insertAndFire(new MonitoringStation("station-1"));
+ insertAndFire(new SensorActivated("sensor-1"));
+ // The AND branch matches: calibration + alarm together, no heartbeat needed.
+ insertAndFire(new CalibrationPassed("sensor-1"));
+ insertAndFire(new AlarmRaised("sensor-1", "high"));
+ insertAndFire(new OperatorAcknowledged("sensor-1", "alice"));
+
+ assertThat(results).containsExactly("acknowledged");
+ }
+
+ @Test
+ public void sequenceDoesNotFireWhenAndPartial() {
+ // Same shape as sequenceFiresWithAndOfTwo, but only one of the AND children fires.
+ // AND requires every child matched; one match is not enough; step never advances.
+ final List<String> results = new ArrayList<>();
+
+ Rule andRule =
+ rule("and-partial-rule").build(
+ pattern(station),
+ sequence(
+ pattern(sensorActivated).expr("anchor", a -> a.getSensorId().equals("sensor-1")),
+ and(
+ pattern(heartbeat).expr("ok", h -> h.getSensorId().equals("sensor-1")),
+ pattern(alarm).expr("hi", al -> al.getSeverity().equals("high"))
+ ),
+ pattern(ack).expr("ack", a -> a.getOperator().equals("alice"))
+ ),
+ execute(() -> results.add("acknowledged"))
+ );
+
+ ksession = KieBaseBuilder.createKieBaseFromModel(new ModelImpl().addRule(andRule)).newKieSession();
+
+ insertAndFire(new MonitoringStation("station-1"));
+ insertAndFire(new SensorActivated("sensor-1"));
+ insertAndFire(new HeartbeatOk("sensor-1")); // AND child #1 fires; child #2 never does.
+ insertAndFire(new OperatorAcknowledged("sensor-1", "alice")); // Step 1 still active; ack is ignored.
+
+ assertThat(results).isEmpty();
+ }
+
+ @Test
+ public void orBranchRetractAfterStepConsumedIsNoOp() {
+ final List<String> localResults = new ArrayList<>();
+
+ Rule orRule =
+ rule("or-retract-noop").build(
+ pattern(station),
+ sequence(
+ pattern(sensorActivated).expr("anchor", a -> a.getSensorId().equals("sensor-1")),
+ or(
+ pattern(heartbeat).expr("ok", h -> h.getSensorId().equals("sensor-1")),
+ pattern(alarm).expr("hi", al -> al.getSeverity().equals("high"))
+ ),
+ pattern(ack).expr("ack", a -> a.getOperator().equals("alice"))
+ ),
+ execute(() -> localResults.add("acknowledged"))
+ );
+
+ ksession = KieBaseBuilder.createKieBaseFromModel(new ModelImpl().addRule(orRule)).newKieSession();
+
+ insertAndFire(new MonitoringStation("station-1"));
+ insertAndFire(new SensorActivated("sensor-1")); // anchor fires; OR step activates
+
+ FactHandle hbHandle = ksession.insert(new HeartbeatOk("sensor-1"));
+ ksession.fireAllRules(); // OR branch fires; step deactivates, ack step activates
+
+ ksession.retract(hbHandle);
+ ksession.fireAllRules(); // no-op: OR step already deactivated
+
+ insertAndFire(new AlarmRaised("sensor-1", "high")); // no-op: OR step gone, no listener
+ insertAndFire(new OperatorAcknowledged("sensor-1", "alice")); // ack step matches; rule fires
+
+ assertThat(localResults).containsExactly("acknowledged");
+ }
+
+ @Test
+ public void orBranchRetractAndReinsertAfterStepConsumedIsNoOp() {
+ final List<String> localResults = new ArrayList<>();
+
+ Rule orRule =
+ rule("or-reinsert-noop").build(
+ pattern(station),
+ sequence(
+ pattern(sensorActivated).expr("anchor", a -> a.getSensorId().equals("sensor-1")),
+ or(
+ pattern(heartbeat).expr("ok", h -> h.getSensorId().equals("sensor-1")),
+ pattern(alarm).expr("hi", al -> al.getSeverity().equals("high"))
+ ),
+ pattern(ack).expr("ack", a -> a.getOperator().equals("alice"))
+ ),
+ execute(() -> localResults.add("acknowledged"))
+ );
+
+ ksession = KieBaseBuilder.createKieBaseFromModel(new ModelImpl().addRule(orRule)).newKieSession();
+
+ insertAndFire(new MonitoringStation("station-1"));
+ insertAndFire(new SensorActivated("sensor-1")); // anchor fires; OR step activates
+
+ FactHandle hbHandle = ksession.insert(new HeartbeatOk("sensor-1"));
+ ksession.fireAllRules(); // OR branch fires; step deactivated, ack step activates
+
+ ksession.retract(hbHandle);
+ ksession.fireAllRules(); // no-op: OR step already deactivated
+
+ insertAndFire(new HeartbeatOk("sensor-1")); // no-op: no listener for OR branch any more
+ insertAndFire(new OperatorAcknowledged("sensor-1", "alice")); // ack step matches; rule fires
+
+ assertThat(localResults).containsExactly("acknowledged");
+ }
+
+ @Test
+ public void nestedOrInsideOrAdvancesOnAnyLeaf() {
+ final List<String> results = new ArrayList<>();
+
+ Rule rule =
+ rule("nested-or-rule").build(
+ pattern(station),
+ sequence(
+ pattern(sensorActivated).expr("anchor", a -> a.getSensorId().equals("sensor-1")),
+ or(
+ or(
+ pattern(heartbeat).expr("hb", h -> h.getSensorId().equals("sensor-1")),
+ pattern(alarm).expr("hi", al -> al.getSeverity().equals("high"))
+ ),
+ pattern(calibration).expr("cal", c -> c.getSensorId().equals("sensor-1"))
+ ),
+ pattern(ack).expr("ack", a -> a.getOperator().equals("alice"))
+ ),
+ execute(() -> results.add("acknowledged"))
+ );
+
+ ksession = KieBaseBuilder.createKieBaseFromModel(new ModelImpl().addRule(rule)).newKieSession();
+
+ insertAndFire(new MonitoringStation("station-1"));
+ insertAndFire(new SensorActivated("sensor-1"));
+ // Trigger the inner-OR branch (alarm), which feeds the outer OR → step advances
+ insertAndFire(new AlarmRaised("sensor-1", "high"));
+ insertAndFire(new OperatorAcknowledged("sensor-1", "alice"));
+
+ assertThat(results).containsExactly("acknowledged");
+ }
+
+ @Test
+ public void andChildReceivesDuplicateSignalDoesNotDoubleCount() {
+ final List<String> results = new ArrayList<>();
+
+ Rule rule =
+ rule("and-dup-rule").build(
+ pattern(station),
+ sequence(
+ pattern(sensorActivated).expr("anchor", a -> a.getSensorId().equals("sensor-1")),
+ and(
+ pattern(heartbeat).expr("hb", h -> h.getSensorId().equals("sensor-1")),
+ pattern(alarm).expr("hi", al -> al.getSeverity().equals("high"))
+ ),
+ pattern(ack).expr("ack", a -> a.getOperator().equals("alice"))
+ ),
+ execute(() -> results.add("acknowledged"))
+ );
+
+ ksession = KieBaseBuilder.createKieBaseFromModel(new ModelImpl().addRule(rule)).newKieSession();
+
+ insertAndFire(new MonitoringStation("station-1"));
+ insertAndFire(new SensorActivated("sensor-1"));
+
+ // AND child #1 arrives twice — must not advance step on second arrival
+ insertAndFire(new HeartbeatOk("sensor-1"));
+ assertThat(results).isEmpty(); // still waiting for alarm
+
+ insertAndFire(new HeartbeatOk("sensor-1")); // duplicate — no-op for the gate
+ assertThat(results).isEmpty();
+
+ // AND child #2 arrives — both distinct children now satisfied
+ insertAndFire(new AlarmRaised("sensor-1", "high"));
+ insertAndFire(new OperatorAcknowledged("sensor-1", "alice"));
+
+ assertThat(results).containsExactly("acknowledged");
+ }
+}
diff --git a/drools-model/drools-model-compiler/src/test/java/org/drools/modelcompiler/PatternDSLSequenceConsequenceTest.java b/drools-model/drools-model-compiler/src/test/java/org/drools/modelcompiler/PatternDSLSequenceConsequenceTest.java
new file mode 100644
index 00000000000..3931065c2c8
--- /dev/null
+++ b/drools-model/drools-model-compiler/src/test/java/org/drools/modelcompiler/PatternDSLSequenceConsequenceTest.java
@@ -0,0 +1,452 @@
+/*
+ * 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.modelcompiler;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.drools.model.Rule;
+import org.drools.model.Variable;
+import org.drools.model.impl.ModelImpl;
+import org.drools.modelcompiler.domain.Person;
+import org.drools.modelcompiler.domain.Relationship;
+import org.drools.modelcompiler.domain.SensorEvents.AlarmRaised;
+import org.drools.modelcompiler.domain.SensorEvents.HeartbeatOk;
+import org.drools.modelcompiler.domain.SensorEvents.MonitoringStation;
+import org.drools.modelcompiler.domain.SensorEvents.SensorActivated;
+import org.drools.modelcompiler.domain.Toy;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+import org.kie.api.KieBase;
+import org.kie.api.runtime.KieSession;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.drools.model.DSL.declarationOf;
+import static org.drools.model.PatternDSL.on;
+import static org.drools.model.PatternDSL.or;
+import static org.drools.model.PatternDSL.pattern;
+import static org.drools.model.PatternDSL.rule;
+import static org.drools.model.PatternDSL.sequence;
+import static org.drools.model.PatternDSL.when;
+
+public class PatternDSLSequenceConsequenceTest {
+
+ private final List<String> results = new ArrayList<>();
+ private KieSession ksession;
+
+ @AfterEach
+ public void tearDown() {
+ results.clear();
+ if (ksession != null) {
+ ksession.dispose();
+ ksession = null;
+ }
+ }
+
+ private void insertAndFire(Object... facts) {
+ for (Object f : facts) { ksession.insert(f); }
+ ksession.fireAllRules();
+ }
+
+ @Test
+ public void anchorVariableBoundInConsequence() {
+ Variable<Person> personV = declarationOf(Person.class);
+ Variable<Toy> toyV = declarationOf(Toy.class);
+ Variable<Relationship> relV = declarationOf(Relationship.class);
+
+ Rule rule = rule("anchor-binding-rule").build(
+ pattern(personV),
+ sequence(
+ pattern(toyV).expr("is-ball", t -> t.getName().equals("ball")),
+ pattern(relV).expr("is-go", r -> r.getStart().equals("go"))
+ ),
+ on(personV).execute(p -> results.add("fired:" + p.getName()))
+ );
+
+ KieBase kbase = KieBaseBuilder.createKieBaseFromModel(new ModelImpl().addRule(rule));
+ ksession = kbase.newKieSession();
+
+ insertAndFire(new Person("alice"));
+ insertAndFire(new Toy("ball"));
+ insertAndFire(new Relationship("go", "done"));
+
+ assertThat(results).containsExactly("fired:alice");
+ }
+
+ @Test
+ public void stepVariableBoundInConsequence() {
+ Variable<Person> personV = declarationOf(Person.class);
+ Variable<Toy> toyV = declarationOf(Toy.class);
+ Variable<Relationship> relV = declarationOf(Relationship.class);
+
+ Rule rule = rule("step-binding-rule").build(
+ pattern(personV),
+ sequence(
+ pattern(toyV).expr("is-ball", t -> t.getName().equals("ball")),
+ pattern(relV).expr("is-go", r -> r.getStart().equals("go"))
+ ),
+ on(toyV, relV).execute((t, r) -> results.add(t.getName() + "+" + r.getStart()))
+ );
+
+ KieBase kbase = KieBaseBuilder.createKieBaseFromModel(new ModelImpl().addRule(rule));
+ ksession = kbase.newKieSession();
+
+ insertAndFire(new Person("anchor"));
+ insertAndFire(new Toy("ball"));
+ insertAndFire(new Relationship("go", "done"));
+
+ assertThat(results).containsExactly("ball+go");
+ }
+
+ @Test
+ public void namedConsequenceAnchorVariableIsAccessible() {
+ Variable<Person> personV = declarationOf(Person.class);
+ Variable<Toy> toyV = declarationOf(Toy.class);
+ Variable<Relationship> relV = declarationOf(Relationship.class);
+
+ Rule rule = rule("named-consequence-anchor").build(
+ pattern(personV),
+ sequence(
+ pattern(toyV).expr("is-ball", t -> t.getName().equals("ball")),
+ pattern(relV).expr("is-go", r -> r.getStart().equals("go"))
+ ),
+ when("cond", personV, p -> p.getAge() < 50).then(
+ on(personV).breaking().execute(p -> results.add("young:" + p.getName()))
+ ).elseWhen().then(
+ on(personV).breaking().execute(p -> results.add("old:" + p.getName()))
+ )
+ );
+
+ KieBase kbase = KieBaseBuilder.createKieBaseFromModel(new ModelImpl().addRule(rule));
+ ksession = kbase.newKieSession();
+
+ insertAndFire(new Person("alice", 25));
+ insertAndFire(new Toy("ball"));
+ insertAndFire(new Relationship("go", "done"));
+
+ assertThat(results).containsExactly("young:alice");
+ }
+
+ @Test
+ public void namedConsequenceStepVariablesAreAccessible() {
+ Variable<Person> personV = declarationOf(Person.class);
+ Variable<Toy> toyV = declarationOf(Toy.class);
+ Variable<Relationship> relV = declarationOf(Relationship.class);
+
+ Rule rule = rule("named-consequence-step-vars").build(
+ pattern(personV),
+ sequence(
+ pattern(toyV).expr("is-ball", t -> t.getName().equals("ball")),
+ pattern(relV).expr("is-go", r -> r.getStart().equals("go"))
+ ),
+ when("cond", personV, p -> p.getAge() < 50).then(
+ on(toyV, relV).breaking().execute((t, r) ->
+ results.add("named:" + t.getName() + "+" + r.getStart()))
+ ).elseWhen().then(
+ on(toyV, relV).breaking().execute((t, r) ->
+ results.add("named-old:" + t.getName() + "+" + r.getStart()))
+ )
+ );
+
+ KieBase kbase = KieBaseBuilder.createKieBaseFromModel(new ModelImpl().addRule(rule));
+ ksession = kbase.newKieSession();
+
+ insertAndFire(new Person("alice", 25));
+ insertAndFire(new Toy("ball"));
+ insertAndFire(new Relationship("go", "done"));
+
+ assertThat(results).containsExactly("named:ball+go");
+ }
+
+ @Test
+ public void stepVarsResolvedWhenPatternChainsAfterSequence() {
+ Variable<Person> personV = declarationOf(Person.class);
+ Variable<Toy> toyV = declarationOf(Toy.class);
+ Variable<Relationship> relV = declarationOf(Relationship.class);
+ Variable<Person> extra = declarationOf(Person.class);
+
+ Rule rule = rule("step-vars-after-chain").build(
+ pattern(personV).expr("is-anchor", p -> p.getName().equals("anchor")),
+ sequence(
+ pattern(toyV).expr("is-ball", t -> t.getName().equals("ball")),
+ pattern(relV).expr("is-go", r -> r.getStart().equals("go"))
+ ),
+ // extra pattern chained AFTER the sequence
+ pattern(extra).expr("is-observer", p -> p.getName().equals("observer")),
+ on(toyV, relV, extra).execute((t, r, p) ->
+ results.add(t.getName() + "+" + r.getStart() + "+" + p.getName()))
+ );
+
+ KieBase kbase = KieBaseBuilder.createKieBaseFromModel(new ModelImpl().addRule(rule));
+ ksession = kbase.newKieSession();
+
+ // Insert the extra fact first so it is available when the sequence completes
+ insertAndFire(new Person("observer"));
+ insertAndFire(new Person("anchor"));
+ insertAndFire(new Toy("ball"));
+ insertAndFire(new Relationship("go", "done"));
+
+ assertThat(results).containsExactly("ball+go+observer");
+ }
+
+ @Test
+ public void stepVariableConsequenceFiredTwice() {
+ Variable<Person> personV = declarationOf(Person.class);
+ Variable<Toy> toyV = declarationOf(Toy.class);
+ Variable<Relationship> relV = declarationOf(Relationship.class);
+
+ Rule rule = rule("step-binding-fires-twice").build(
+ pattern(personV),
+ sequence(
+ pattern(toyV).expr("is-ball", t -> t.getName().equals("ball")),
+ pattern(relV).expr("is-go", r -> r.getStart().equals("go"))
+ ),
+ on(toyV, relV).execute((t, r) -> results.add(t.getName() + "+" + r.getStart()))
+ );
+
+ KieBase kbase = KieBaseBuilder.createKieBaseFromModel(new ModelImpl().addRule(rule));
+ ksession = kbase.newKieSession();
+
+ // Two anchors — rule fires once per anchor, second fire hits fetchFacts()
+ insertAndFire(new Person("alice"));
+ insertAndFire(new Person("bob"));
+ insertAndFire(new Toy("ball"));
+ insertAndFire(new Relationship("go", "done"));
+
+ assertThat(results).containsExactlyInAnyOrder("ball+go", "ball+go");
+ }
+
+ @Test
+ public void stepVariableWithDroolsInConsequence() {
+ Variable<Person> personV = declarationOf(Person.class);
+ Variable<Toy> toyV = declarationOf(Toy.class);
+ Variable<Relationship> relV = declarationOf(Relationship.class);
+
+ Rule rule = rule("step-binding-with-drools").build(
+ pattern(personV),
+ sequence(
+ pattern(toyV).expr("is-ball", t -> t.getName().equals("ball")),
+ pattern(relV).expr("is-go", r -> r.getStart().equals("go"))
+ ),
+ on(toyV).execute((drools, t) -> results.add(t.getName()))
+ );
+
+ KieBase kbase = KieBaseBuilder.createKieBaseFromModel(new ModelImpl().addRule(rule));
+ ksession = kbase.newKieSession();
+
+ insertAndFire(new Person("anchor"));
+ insertAndFire(new Toy("ball"));
+ insertAndFire(new Relationship("go", "done"));
+
+ assertThat(results).containsExactly("ball");
+ }
+
+ @Test
+ public void orStepSecondBranchVariableIsAccessibleInConsequence() {
+ Variable<MonitoringStation> stationV = declarationOf(MonitoringStation.class);
+ Variable<SensorActivated> activatedV = declarationOf(SensorActivated.class);
+ Variable<HeartbeatOk> heartbeatV = declarationOf(HeartbeatOk.class);
+ Variable<AlarmRaised> alarmV = declarationOf(AlarmRaised.class);
+
+ Rule rule = rule("or-step-second-branch").build(
+ pattern(stationV),
+ sequence(
+ pattern(activatedV).expr("act", a -> a.getSensorId().equals("s1")),
+ or(
+ pattern(heartbeatV).expr("hb", h -> h.getSensorId().equals("s1")),
+ pattern(alarmV).expr("hi", al -> al.getSeverity().equals("high"))
+ )
+ ),
+ on(alarmV).execute(al -> results.add("alarm:" + al.getSeverity()))
+ );
+
+ KieBase kbase = KieBaseBuilder.createKieBaseFromModel(new ModelImpl().addRule(rule));
+ ksession = kbase.newKieSession();
+
+ insertAndFire(new MonitoringStation("s1"));
+ insertAndFire(new SensorActivated("s1"));
+ insertAndFire(new AlarmRaised("s1", "high"));
+
+ assertThat(results).containsExactly("alarm:high");
+ }
+
+ @Test
+ public void orStepFirstBranchVariableIsAccessibleInConsequence() {
+ Variable<MonitoringStation> stationV = declarationOf(MonitoringStation.class);
+ Variable<SensorActivated> activatedV = declarationOf(SensorActivated.class);
+ Variable<HeartbeatOk> heartbeatV = declarationOf(HeartbeatOk.class);
+ Variable<AlarmRaised> alarmV = declarationOf(AlarmRaised.class);
+
+ Rule rule = rule("or-step-first-branch").build(
+ pattern(stationV),
+ sequence(
+ pattern(activatedV).expr("act", a -> a.getSensorId().equals("s1")),
+ or(
+ pattern(heartbeatV).expr("hb", h -> h.getSensorId().equals("s1")),
+ pattern(alarmV).expr("hi", al -> al.getSeverity().equals("high"))
+ )
+ ),
+ on(heartbeatV).execute(h -> results.add("heartbeat:" + h.getSensorId()))
+ );
+
+ KieBase kbase = KieBaseBuilder.createKieBaseFromModel(new ModelImpl().addRule(rule));
+ ksession = kbase.newKieSession();
+
+ insertAndFire(new MonitoringStation("s1"));
+ insertAndFire(new SensorActivated("s1"));
+ insertAndFire(new HeartbeatOk("s1")); // first OR child — filterIndex 1, appended at position 1
+
+ assertThat(results).containsExactly("heartbeat:s1");
+ }
+
+ @Test
+ public void twoSequencesInOneRuleStepVariablesResolveFromCorrectMemory() {
+ Variable<Person> personV = declarationOf(Person.class);
+ Variable<Toy> toyV = declarationOf(Toy.class); // seq1, filterIndex=0
+ Variable<Relationship> relV = declarationOf(Relationship.class); // seq2, filterIndex=0 (collision)
+
+ Rule rule = rule("two-sequences-variable-collision").build(
+ pattern(personV),
+ sequence(
+ pattern(toyV).expr("is-ball", t -> t.getName().equals("ball"))
+ ),
+ sequence(
+ pattern(relV).expr("is-go", r -> r.getStart().equals("go"))
+ ),
+ on(toyV, relV).execute((t, r) -> results.add(t.getName() + "+" + r.getStart()))
+ );
+
+ KieBase kbase = KieBaseBuilder.createKieBaseFromModel(new ModelImpl().addRule(rule));
+ ksession = kbase.newKieSession();
+
+ insertAndFire(new Person("anchor"));
+ insertAndFire(new Toy("ball"));
+ insertAndFire(new Relationship("go", "done"));
+
+ assertThat(results).containsExactly("ball+go");
+ }
+
+ @Test
+ public void twoSequencesInOneRuleMultipleAnchorsBothFire() {
+ Variable<Person> personV = declarationOf(Person.class);
+ Variable<Toy> toyV = declarationOf(Toy.class);
+ Variable<Relationship> relV = declarationOf(Relationship.class);
+
+ Rule rule = rule("two-sequences-multiple-anchors").build(
+ pattern(personV),
+ sequence(
+ pattern(toyV).expr("is-ball", t -> t.getName().equals("ball"))
+ ),
+ sequence(
+ pattern(relV).expr("is-go", r -> r.getStart().equals("go"))
+ ),
+ on(personV, toyV, relV).execute((p, t, r) -> results.add(p.getName() + ":" + t.getName() + "+" + r.getStart()))
+ );
+
+ KieBase kbase = KieBaseBuilder.createKieBaseFromModel(new ModelImpl().addRule(rule));
+ ksession = kbase.newKieSession();
+
+ // Multiple anchors (Person)
+ insertAndFire(new Person("alice"));
+ insertAndFire(new Person("bob"));
+ insertAndFire(new Toy("ball"));
+ insertAndFire(new Relationship("go", "done"));
+
+ assertThat(results).containsExactlyInAnyOrder(
+ "alice:ball+go",
+ "bob:ball+go"
+ );
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {"single-anchor", "two-anchors", "three-anchors"})
+ public void twoSequencesInOneRuleMultipleAnchorsAndBindings(String mode) {
+ Variable<Person> personV = declarationOf(Person.class);
+ Variable<Toy> toyV = declarationOf(Toy.class);
+ Variable<Relationship> relV = declarationOf(Relationship.class);
+
+ Rule rule = rule("two-sequences-" + mode).build(
+ pattern(personV),
+ sequence(
+ pattern(toyV).expr("is-ball", t -> t.getName().equals("ball"))
+ ),
+ sequence(
+ pattern(relV).expr("is-go", r -> r.getStart().equals("go"))
+ ),
+ on(personV, toyV, relV).execute((p, t, r) -> results.add(p.getName() + ":" + t.getName() + "+" + r.getStart()))
+ );
+
+ KieBase kbase = KieBaseBuilder.createKieBaseFromModel(new ModelImpl().addRule(rule));
+ ksession = kbase.newKieSession();
+
+ switch (mode) {
+ case "single-anchor":
+ insertAndFire(new Person("alice"));
+ insertAndFire(new Toy("ball"));
+ insertAndFire(new Relationship("go", "done"));
+ assertThat(results).containsExactly("alice:ball+go");
+ break;
+ case "two-anchors":
+ insertAndFire(new Person("alice"));
+ insertAndFire(new Person("bob"));
+ insertAndFire(new Toy("ball"));
+ insertAndFire(new Relationship("go", "done"));
+ assertThat(results).containsExactlyInAnyOrder("alice:ball+go", "bob:ball+go");
+ break;
+ case "three-anchors":
+ insertAndFire(new Person("alice"));
+ insertAndFire(new Person("bob"));
+ insertAndFire(new Person("charlie"));
+ insertAndFire(new Toy("ball"));
+ insertAndFire(new Relationship("go", "done"));
+ assertThat(results).containsExactlyInAnyOrder("alice:ball+go", "bob:ball+go", "charlie:ball+go");
+ break;
+ }
+ }
+
+ @Test
+ public void stepVariableResolvedWhenLhsHasFirstLevelOr() {
+ Variable<Person> personV = declarationOf(Person.class);
+ Variable<Toy> toyV = declarationOf(Toy.class);
+ Variable<Relationship> relV = declarationOf(Relationship.class);
+
+ Rule rule = rule("step-var-with-first-level-or").build(
+ or(
+ pattern(personV).expr("is-alice", p -> p.getName().equals("alice")),
+ pattern(personV).expr("is-bob", p -> p.getName().equals("bob"))
+ ),
+ sequence(
+ pattern(toyV).expr("is-ball", t -> t.getName().equals("ball")),
+ pattern(relV).expr("is-go", r -> r.getStart().equals("go"))
+ ),
+ on(toyV, relV).execute((t, r) -> results.add(t.getName() + "+" + r.getStart()))
+ );
+
+ KieBase kbase = KieBaseBuilder.createKieBaseFromModel(new ModelImpl().addRule(rule));
+ ksession = kbase.newKieSession();
+
+ insertAndFire(new Person("alice"));
+ insertAndFire(new Toy("ball"));
+ insertAndFire(new Relationship("go", "done"));
+
+ assertThat(results).containsExactly("ball+go");
+ }
+}
diff --git a/drools-model/drools-model-compiler/src/test/java/org/drools/modelcompiler/PatternDSLSequenceEdgeCaseTest.java b/drools-model/drools-model-compiler/src/test/java/org/drools/modelcompiler/PatternDSLSequenceEdgeCaseTest.java
new file mode 100644
index 00000000000..a499e0dd2c8
--- /dev/null
+++ b/drools-model/drools-model-compiler/src/test/java/org/drools/modelcompiler/PatternDSLSequenceEdgeCaseTest.java
@@ -0,0 +1,514 @@
+/*
+ * 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.modelcompiler;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.drools.model.Rule;
+import org.drools.model.Variable;
+import org.drools.model.impl.ModelImpl;
+import org.drools.modelcompiler.domain.Person;
+import org.drools.modelcompiler.domain.Relationship;
+import org.drools.modelcompiler.domain.Result;
+import org.drools.modelcompiler.domain.SensorEvents.AlarmRaised;
+import org.drools.modelcompiler.domain.SensorEvents.HeartbeatOk;
+import org.drools.modelcompiler.domain.SensorEvents.MonitoringStation;
+import org.drools.modelcompiler.domain.SensorEvents.SensorActivated;
+import org.drools.modelcompiler.domain.Toy;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+import org.kie.api.KieBase;
+import org.kie.api.runtime.KieSession;
+import org.kie.api.runtime.rule.FactHandle;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.drools.model.DSL.declarationOf;
+import static org.drools.model.DSL.execute;
+import static org.drools.model.PatternDSL.and;
+import static org.drools.model.PatternDSL.on;
+import static org.drools.model.PatternDSL.or;
+import static org.drools.model.PatternDSL.pattern;
+import static org.drools.model.PatternDSL.rule;
+import static org.drools.model.PatternDSL.sequence;
+import static org.drools.model.PatternDSL.when;
+
+public class PatternDSLSequenceEdgeCaseTest {
+
+ private final List<String> results = new ArrayList<>();
+ private KieSession ksession;
+
+ @AfterEach
+ public void tearDown() {
+ results.clear();
+ if (ksession != null) {
+ ksession.dispose();
+ ksession = null;
+ }
+ }
+
+ @Test
+ public void consequenceBranchDoesNotFallThroughToSequence() {
+ Variable<Result> resultV = declarationOf(Result.class);
+ Variable<Person> personV = declarationOf(Person.class);
+ Variable<Toy> toyV = declarationOf(Toy.class);
+ Variable<Relationship> relV = declarationOf(Relationship.class);
+
+ Rule rule = rule("c1-rule").build(
+ pattern(resultV),
+ pattern(personV),
+ when("cond", personV, p -> p.getAge() < 30).then(
+ on(personV, resultV).breaking().execute((p, r) -> r.setValue("young"))
+ ).elseWhen().then(
+ on(personV, resultV).breaking().execute((p, r) -> r.setValue("other"))
+ ),
+ sequence(
+ pattern(toyV).expr("is-ball", t -> t.getName().equals("ball")),
+ pattern(relV).expr("is-go", r -> r.getStart().equals("go"))
+ ),
+ execute(() -> results.add("seq-fired"))
+ );
+
+ KieBase kieBase = KieBaseBuilder.createKieBaseFromModel(new ModelImpl().addRule(rule));
+ assertThat(kieBase).isNotNull();
+ }
+
+ @Test
+ public void longSequenceDoesNotOverflowCircularBuffer() {
+ Variable<Person> personV = declarationOf(Person.class);
+ Variable<Toy> toyV = declarationOf(Toy.class);
+
+ Rule rule = rule("c2-rule").build(
+ pattern(personV),
+ sequence(
+ pattern(toyV).expr("is-ball", t -> t.getName().equals("ball"))
+ ),
+ execute(() -> results.add("fired"))
+ );
+
+ KieBase kbase = KieBaseBuilder.createKieBaseFromModel(new ModelImpl().addRule(rule));
+ ksession = kbase.newKieSession();
+
+ Person anchor = new Person("anchor");
+ FactHandle anchorHandle = ksession.insert(anchor);
+ ksession.fireAllRules();
+
+ for (int i = 0; i < 110; i++) {
+ ksession.insert(new Toy("ball"));
+ ksession.fireAllRules(); // sequence completes
+ ksession.update(anchorHandle, anchor); // restart sequencer
+ ksession.fireAllRules();
+ }
+
+ assertThat(results).hasSize(110);
+ }
+
+ @Test
+ public void retractAnchorWithOrStepDoesNotLeakSiblingAdapters() {
+ Variable<MonitoringStation> stationV = declarationOf(MonitoringStation.class);
+ Variable<SensorActivated> activatedV = declarationOf(SensorActivated.class);
+ Variable<HeartbeatOk> heartbeatV = declarationOf(HeartbeatOk.class);
+ Variable<AlarmRaised> alarmV = declarationOf(AlarmRaised.class);
+
+ Rule rule = rule("c3-rule").build(
+ pattern(stationV),
+ sequence(
+ pattern(activatedV).expr("s1", a -> a.getSensorId().equals("s1")),
+ or(
+ pattern(heartbeatV).expr("hb", h -> h.getSensorId().equals("s1")),
+ pattern(alarmV).expr("al", al -> al.getSeverity().equals("high"))
+ )
+ ),
+ execute(() -> results.add("seq-fired"))
+ );
+
+ KieBase kbase = KieBaseBuilder.createKieBaseFromModel(new ModelImpl().addRule(rule));
+ ksession = kbase.newKieSession();
+
+ FactHandle stationHandle = ksession.insert(new MonitoringStation("station-1"));
+ ksession.fireAllRules(); // sequencer starts
+
+ ksession.insert(new SensorActivated("s1"));
+ ksession.fireAllRules(); // step-1 consumed; OR step now active
+
+ // Retract the anchor — stop() must deactivate BOTH OR siblings
+ ksession.retract(stationHandle);
+ ksession.fireAllRules();
+
+ // Insert a heartbeat — the leaked OR sibling adapter must NOT respond
+ ksession.insert(new HeartbeatOk("s1"));
+ ksession.fireAllRules();
+
+ assertThat(results).isEmpty();
+ }
+
+ @Test
+ public void retractAnchorMidAndStepDoesNotLeakChildAdapters() {
+ Variable<MonitoringStation> stationV = declarationOf(MonitoringStation.class);
+ Variable<SensorActivated> activatedV = declarationOf(SensorActivated.class);
+ Variable<HeartbeatOk> heartbeatV = declarationOf(HeartbeatOk.class);
+ Variable<AlarmRaised> alarmV = declarationOf(AlarmRaised.class);
+
+ Rule rule = rule("c3b-rule").build(
+ pattern(stationV),
+ sequence(
+ pattern(activatedV).expr("s1", a -> a.getSensorId().equals("s1")),
+ and(
+ pattern(heartbeatV).expr("hb", h -> h.getSensorId().equals("s1")),
+ pattern(alarmV).expr("al", al -> al.getSeverity().equals("high"))
+ )
+ ),
+ execute(() -> results.add("seq-fired"))
+ );
+
+ KieBase kbase = KieBaseBuilder.createKieBaseFromModel(new ModelImpl().addRule(rule));
+ ksession = kbase.newKieSession();
+
+ FactHandle stationHandle = ksession.insert(new MonitoringStation("station-1"));
+ ksession.fireAllRules(); // sequencer starts
+
+ ksession.insert(new SensorActivated("s1"));
+ ksession.fireAllRules(); // step-1 consumed; AND step now active
+
+ // Satisfy the first AND child only — AND step is now partially satisfied
+ ksession.insert(new HeartbeatOk("s1"));
+ ksession.fireAllRules();
+ assertThat(results).isEmpty(); // still waiting for alarm
+
+ // Retract the anchor — stop() must deactivate BOTH AND children
+ ksession.retract(stationHandle);
+ ksession.fireAllRules();
+
+ // Insert the second AND child — the AND adapter must NOT respond
+ ksession.insert(new AlarmRaised("s1", "high"));
+ ksession.fireAllRules();
+
+ assertThat(results).isEmpty();
+ }
+
+ @Test
+ public void twoAnchorsOrStepOnlyOneAnchorSatisfied() {
+ Variable<MonitoringStation> stationV = declarationOf(MonitoringStation.class);
+ Variable<SensorActivated> activatedAV = declarationOf(SensorActivated.class);
+ Variable<SensorActivated> activatedBV = declarationOf(SensorActivated.class);
+ Variable<HeartbeatOk> heartbeatV = declarationOf(HeartbeatOk.class);
+ Variable<AlarmRaised> alarmV = declarationOf(AlarmRaised.class);
+
+ // Rule anchored on MonitoringStation: sequence is SensorActivated → or(Heartbeat, Alarm)
+ Rule rule = rule("c5-rule").build(
+ pattern(stationV),
+ sequence(
+ pattern(activatedAV).expr("s1-act", a -> a.getSensorId().equals("s1")),
+ or(
+ pattern(heartbeatV).expr("hb", h -> h.getSensorId().equals("s1")),
+ pattern(alarmV).expr("al", al -> al.getSeverity().equals("high"))
+ )
+ ),
+ execute(() -> results.add("seq-fired"))
+ );
+
+ KieBase kbase = KieBaseBuilder.createKieBaseFromModel(new ModelImpl().addRule(rule));
+ ksession = kbase.newKieSession();
+
+ // Start two independent sequencers via two separate anchor tuples
+ ksession.insert(new MonitoringStation("station-A"));
+ ksession.insert(new MonitoringStation("station-B"));
+ ksession.fireAllRules();
+
+ // Both sequencers consume step-1 (SensorActivated s1); both are now at the OR step
+ ksession.insert(new SensorActivated("s1"));
+ ksession.fireAllRules();
+ assertThat(results).isEmpty(); // OR step not yet satisfied for either
+
+ // OR step satisfied — both sequencers complete; each must fire exactly once
+ ksession.insert(new HeartbeatOk("s1"));
+ ksession.fireAllRules();
+
+ assertThat(results).hasSize(2);
+ }
+
+ @Test
+ public void twoSequencesInOneRuleBothComplete() {
+ Variable<Person> personV = declarationOf(Person.class);
+ Variable<Toy> toyV = declarationOf(Toy.class);
+ Variable<Relationship> relV = declarationOf(Relationship.class);
+
+ Rule rule = rule("c4-two-seq-rule").build(
+ pattern(personV),
+ sequence(
+ pattern(toyV).expr("is-ball", t -> t.getName().equals("ball"))
+ ),
+ sequence(
+ pattern(relV).expr("is-go", r -> r.getStart().equals("go"))
+ ),
+ execute(() -> results.add("fired"))
+ );
+
+ KieBase kbase = KieBaseBuilder.createKieBaseFromModel(new ModelImpl().addRule(rule));
+ ksession = kbase.newKieSession();
+
+ ksession.insert(new Person("anchor"));
+ ksession.fireAllRules(); // both sequencers start
+
+ ksession.insert(new Toy("ball"));
+ ksession.fireAllRules(); // first sequence completes
+
+ // Both SequenceNodes have independent state. Completing the first sequence
+ // must not affect the second; the rule fires only after both complete.
+ ksession.insert(new Relationship("go", "done"));
+ ksession.fireAllRules(); // second sequence completes
+
+ assertThat(results).containsExactly("fired");
+ }
+
+ @Test
+ public void twoRulesWithSequencesTrackIndependently() {
+ Variable<MonitoringStation> stationAV = declarationOf(MonitoringStation.class);
+ Variable<MonitoringStation> stationBV = declarationOf(MonitoringStation.class);
+ Variable<SensorActivated> activated1V = declarationOf(SensorActivated.class);
+ Variable<SensorActivated> activated2V = declarationOf(SensorActivated.class);
+ Variable<HeartbeatOk> heartbeat1V = declarationOf(HeartbeatOk.class);
+ Variable<HeartbeatOk> heartbeat2V = declarationOf(HeartbeatOk.class);
+
+ // Rule 1: station-A → sequence(sensorActivated(s1) → heartbeat(s1))
+ Rule rule1 = rule("c4-rule-1").build(
+ pattern(stationAV).expr("is-a", s -> s.getId().equals("station-A")),
+ sequence(
+ pattern(activated1V).expr("s1-act", a -> a.getSensorId().equals("s1")),
+ pattern(heartbeat1V).expr("s1-hb", h -> h.getSensorId().equals("s1"))
+ ),
+ execute(() -> results.add("rule-1-fired"))
+ );
+
+ // Rule 2: station-B → sequence(sensorActivated(s2) → heartbeat(s2))
+ Rule rule2 = rule("c4-rule-2").build(
+ pattern(stationBV).expr("is-b", s -> s.getId().equals("station-B")),
+ sequence(
+ pattern(activated2V).expr("s2-act", a -> a.getSensorId().equals("s2")),
+ pattern(heartbeat2V).expr("s2-hb", h -> h.getSensorId().equals("s2"))
+ ),
+ execute(() -> results.add("rule-2-fired"))
+ );
+
+ KieBase kbase = KieBaseBuilder.createKieBaseFromModel(
+ new ModelImpl().addRule(rule1).addRule(rule2));
+ ksession = kbase.newKieSession();
+
+ // Start both sequencers
+ ksession.insert(new MonitoringStation("station-A"));
+ ksession.insert(new MonitoringStation("station-B"));
+ ksession.fireAllRules();
+
+ // Complete rule-1's sequence
+ ksession.insert(new SensorActivated("s1"));
+ ksession.fireAllRules();
+ ksession.insert(new HeartbeatOk("s1"));
+ ksession.fireAllRules();
+
+ // Complete rule-2's sequence
+ ksession.insert(new SensorActivated("s2"));
+ ksession.fireAllRules();
+ ksession.insert(new HeartbeatOk("s2"));
+ ksession.fireAllRules();
+
+ // Both rules must have fired exactly once
+ assertThat(results).containsExactlyInAnyOrder("rule-1-fired", "rule-2-fired");
+ }
+
+ @Test
+ public void twoRulesWithSharedAnchorAndDifferentSequencesFireIndependently() {
+ // Both rules anchor on an undecorated pattern(person) — they will share the same
+ // LeftInputAdapterNode, which is the minimal condition to trigger the node-reuse bug.
+ Variable<Person> personV1 = declarationOf(Person.class);
+ Variable<Person> personV2 = declarationOf(Person.class);
+ Variable<Toy> toyV1 = declarationOf(Toy.class);
+ Variable<Toy> toyV2 = declarationOf(Toy.class);
+ Variable<Relationship> relV1 = declarationOf(Relationship.class);
+ Variable<Relationship> relV2 = declarationOf(Relationship.class);
+
+ // Rule 1: sequence(toy named "ball" → relationship starting "go")
+ Rule rule1 = rule("shared-anchor-seq-1").build(
+ pattern(personV1),
+ sequence(
+ pattern(toyV1).expr("is-ball", t -> t.getName().equals("ball")),
+ pattern(relV1).expr("is-go", r -> r.getStart().equals("go"))
+ ),
+ execute(() -> results.add("rule-1-fired"))
+ );
+
+ // Rule 2: same anchor type, completely different step predicates.
+ // With the bug, this SequenceNode is discarded and rule-2 is wired to rule-1's
+ // Sequencer, so it never fires its own consequence.
+ Rule rule2 = rule("shared-anchor-seq-2").build(
+ pattern(personV2),
+ sequence(
+ pattern(toyV2).expr("is-robot", t -> t.getName().equals("robot")),
+ pattern(relV2).expr("is-stop", r -> r.getStart().equals("stop"))
+ ),
+ execute(() -> results.add("rule-2-fired"))
+ );
+
+ KieBase kbase = KieBaseBuilder.createKieBaseFromModel(
+ new ModelImpl().addRule(rule1).addRule(rule2));
+ ksession = kbase.newKieSession();
+
+ // Insert the shared anchor — starts both sequencers
+ ksession.insert(new Person("anchor"));
+ ksession.fireAllRules();
+
+ // Complete rule-1's sequence only
+ ksession.insert(new Toy("ball"));
+ ksession.fireAllRules();
+ ksession.insert(new Relationship("go", "done"));
+ ksession.fireAllRules();
+
+ assertThat(results)
+ .as("rule-1 must fire after its own sequence completes")
+ .contains("rule-1-fired");
+ assertThat(results)
+ .as("rule-2 must not fire — its step predicates were not satisfied")
+ .doesNotContain("rule-2-fired");
+
+ // Now complete rule-2's sequence
+ ksession.insert(new Toy("robot"));
+ ksession.fireAllRules();
+ ksession.insert(new Relationship("stop", "done"));
+ ksession.fireAllRules();
+
+ assertThat(results)
+ .as("rule-2 must fire after its own sequence completes")
+ .containsExactlyInAnyOrder("rule-1-fired", "rule-2-fired");
+ }
+
+ @Test
+ public void betaExprInSequenceStepFiltersAgainstAnchor() {
+ Variable<Person> personV = declarationOf(Person.class);
+ Variable<Toy> toyV = declarationOf(Toy.class);
+
+ Rule rule = rule("beta-in-seq").build(
+ pattern(personV),
+ sequence(
+ pattern(toyV).expr("name-matches-anchor", personV,
+ (t, p) -> t.getName().equals(p.getName()))
+ ),
+ on(personV).execute(p -> results.add("fired:" + p.getName()))
+ );
+
+ KieBase kbase = KieBaseBuilder.createKieBaseFromModel(new ModelImpl().addRule(rule));
+ ksession = kbase.newKieSession();
+
+ ksession.insert(new Person("alice"));
+ ksession.fireAllRules();
+
+ ksession.insert(new Toy("bob")); // name does NOT match — must not fire
+ ksession.fireAllRules();
+ assertThat(results).isEmpty();
+
+ ksession.insert(new Toy("alice")); // name matches — must fire
+ ksession.fireAllRules();
+ assertThat(results).containsExactly("fired:alice");
+ }
+
+ @Test
+ public void betaExprInSequenceStepOnlyMatchesOwnAnchor() {
+ Variable<Person> personV = declarationOf(Person.class);
+ Variable<Toy> toyV = declarationOf(Toy.class);
+
+ Rule rule = rule("beta-in-seq-multi-anchor").build(
+ pattern(personV),
+ sequence(
+ pattern(toyV).expr("name-matches-anchor", personV,
+ (t, p) -> t.getName().equals(p.getName()))
+ ),
+ on(personV).execute(p -> results.add("fired:" + p.getName()))
+ );
+
+ KieBase kbase = KieBaseBuilder.createKieBaseFromModel(new ModelImpl().addRule(rule));
+ ksession = kbase.newKieSession();
+
+ ksession.insert(new Person("alice"));
+ ksession.insert(new Person("bob"));
+ ksession.fireAllRules(); // both sequencers start
+
+ ksession.insert(new Toy("alice")); // only alice's sequencer should advance
+ ksession.fireAllRules();
+ assertThat(results).containsExactly("fired:alice");
+
+ ksession.insert(new Toy("bob")); // only bob's sequencer should advance
+ ksession.fireAllRules();
+ assertThat(results).containsExactlyInAnyOrder("fired:alice", "fired:bob");
+ }
+
+ @Test
+ public void twoRulesWithIdenticalSequenceSegmentBothFire() {
+ // Two rules share the exact same anchor type and identical step predicates.
+ // When the sequence fires, smem.getPathMemories() contains two PathMemory entries
+ // (one per rule). SequencerMemoryImpl.match() flushes only get(0), so only
+ // rule-1's consequence is triggered — rule-2 is silently dropped.
+ Variable<Person> personV1 = declarationOf(Person.class);
+ Variable<Person> personV2 = declarationOf(Person.class);
+ Variable<Toy> toyV1 = declarationOf(Toy.class);
+ Variable<Toy> toyV2 = declarationOf(Toy.class);
+
+ Rule rule1 = rule("shared-seg-1").build(
+ pattern(personV1),
+ sequence(
+ pattern(toyV1).expr("is-ball-1", t -> t.getName().equals("ball"))
+ ),
+ execute(() -> results.add("rule-1-fired"))
+ );
+
+ Rule rule2 = rule("shared-seg-2").build(
+ pattern(personV2),
+ sequence(
+ pattern(toyV2).expr("is-ball-2", t -> t.getName().equals("ball"))
+ ),
+ execute(() -> results.add("rule-2-fired"))
+ );
+
+ KieBase kbase = KieBaseBuilder.createKieBaseFromModel(
+ new ModelImpl().addRule(rule1).addRule(rule2));
+ ksession = kbase.newKieSession();
+
+ ksession.insert(new Person("anchor"));
+ ksession.fireAllRules(); // both sequencers start
+
+ ksession.insert(new Toy("ball"));
+ ksession.fireAllRules(); // sequence completes — both rules must fire
+
+ assertThat(results)
+ .as("both rules whose sequences completed must fire")
+ .containsExactlyInAnyOrder("rule-1-fired", "rule-2-fired");
+ }
+
+ @Test
+ public void emptySequenceThrowsIllegalArgument() {
+ assertThatThrownBy(() -> sequence())
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("at least one step");
+ }
+
+ @Test
+ public void nullStepsThrowsIllegalArgument() {
+ assertThatThrownBy(() -> sequence((org.drools.model.SequenceStep[]) null))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("at least one step");
+ }
+}
diff --git a/drools-model/drools-model-compiler/src/test/java/org/drools/modelcompiler/PatternDSLSequenceLifecycleTest.java b/drools-model/drools-model-compiler/src/test/java/org/drools/modelcompiler/PatternDSLSequenceLifecycleTest.java
new file mode 100644
index 00000000000..3493fdb9d42
--- /dev/null
+++ b/drools-model/drools-model-compiler/src/test/java/org/drools/modelcompiler/PatternDSLSequenceLifecycleTest.java
@@ -0,0 +1,298 @@
+/*
+ * 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.modelcompiler;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.drools.model.Model;
+import org.drools.model.Rule;
+import org.drools.model.Variable;
+import org.drools.model.impl.ModelImpl;
+import org.drools.modelcompiler.domain.Person;
+import org.drools.modelcompiler.domain.Relationship;
+import org.drools.modelcompiler.domain.Toy;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+import org.kie.api.KieBase;
+import org.kie.api.runtime.KieSession;
+import org.kie.api.runtime.rule.FactHandle;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.drools.model.DSL.declarationOf;
+import static org.drools.model.DSL.execute;
+import static org.drools.model.PatternDSL.pattern;
+import static org.drools.model.PatternDSL.rule;
+import static org.drools.model.PatternDSL.sequence;
+
+public class PatternDSLSequenceLifecycleTest {
+
+ private final Variable<Person> person = declarationOf(Person.class);
+ private final Variable<Toy> toy = declarationOf(Toy.class);
+ private final Variable<Relationship> relationship = declarationOf(Relationship.class);
+
+ private final List<String> results = new ArrayList<>();
+ private KieSession ksession;
+
+ @AfterEach
+ public void tearDown() {
+ results.clear();
+ if (ksession != null) {
+ ksession.dispose();
+ ksession = null;
+ }
+ }
+
+ @Test
+ public void retractAnchorStopsSequencer() {
+ ksession = makeKSession();
+
+ FactHandle anchorHandle = ksession.insert(new Person("anchor"));
+ ksession.fireAllRules(); // activates sequencer
+
+ ksession.retract(anchorHandle);
+ ksession.fireAllRules(); // stop() must be called
+
+ ksession.insert(new Toy("ball"));
+ ksession.fireAllRules(); // step-1 — sequencer gone, must not fire
+
+ assertThat(results).isEmpty();
+ }
+
+ @Test
+ public void retractAnchorMidSequenceStopped() {
+ ksession = makeKSession();
+
+ FactHandle anchorHandle = ksession.insert(new Person("anchor"));
+ ksession.fireAllRules();
+
+ ksession.insert(new Toy("ball"));
+ ksession.fireAllRules(); // step 1 consumed, step 2 waiting
+
+ ksession.retract(anchorHandle);
+ ksession.fireAllRules();
+
+ ksession.insert(new Relationship("go", "done"));
+ ksession.fireAllRules(); // step-2 — sequencer stopped, must not fire
+
+ assertThat(results).isEmpty();
+ }
+
+ @Test
+ public void twoAnchorsConcurrentlyBothFire() {
+ ksession = makeKSession();
+
+ ksession.insert(new Person("anchor-A"));
+ ksession.insert(new Person("anchor-B"));
+ ksession.fireAllRules(); // both sequencers start
+
+ ksession.insert(new Toy("ball"));
+ ksession.fireAllRules(); // step-1 consumed by BOTH sequencers
+
+ ksession.insert(new Relationship("go", "done"));
+ ksession.fireAllRules(); // step-2 consumed by BOTH sequencers
+
+ assertThat(results).hasSize(2);
+ }
+
+ @Test
+ public void twoAnchorsSingleStepSequenceBothFire() {
+ ksession = makeKSessionSingleStep();
+
+ ksession.insert(new Person("anchor-A"));
+ ksession.insert(new Person("anchor-B"));
+ ksession.fireAllRules(); // both single-step sequencers start
+
+ ksession.insert(new Toy("ball"));
+ ksession.fireAllRules();
+
+ assertThat(results)
+ .as("each anchor's sequence must complete and fire the rule independently")
+ .hasSize(2);
+ }
+
+ @Test
+ public void sequenceRestartAfterUpdate() {
+ ksession = makeKSession();
+
+ Person anchor = new Person("anchor");
+ FactHandle anchorHandle = ksession.insert(anchor);
+ ksession.fireAllRules();
+
+ ksession.insert(new Toy("ball"));
+ ksession.fireAllRules(); // step 1 consumed
+
+ ksession.update(anchorHandle, anchor); // update restarts the sequence
+ ksession.fireAllRules();
+
+ ksession.insert(new Relationship("go", "done"));
+ ksession.fireAllRules(); // step-2 without a new step-1 — must NOT fire
+
+ assertThat(results).isEmpty();
+ }
+
+ @Test
+ public void sequenceDoesNotFireAgainAfterCompletion() {
+ ksession = makeKSession();
+
+ ksession.insert(new Person("anchor"));
+ ksession.fireAllRules();
+
+ ksession.insert(new Toy("ball"));
+ ksession.fireAllRules();
+
+ ksession.insert(new Relationship("go", "done"));
+ ksession.fireAllRules(); // sequence complete — fired once
+
+ assertThat(results).containsExactly("fired");
+
+ // A second step-1 event after completion must not restart the sequence
+ ksession.insert(new Toy("ball"));
+ ksession.fireAllRules();
+
+ assertThat(results).hasSize(1); // still only one firing
+ }
+
+ @Test
+ public void sequenceRefirableAfterUpdateFollowingCompletion() {
+ ksession = makeKSession();
+
+ Person anchor = new Person("anchor");
+ FactHandle anchorHandle = ksession.insert(anchor);
+ ksession.fireAllRules();
+
+ // First run: complete the sequence
+ ksession.insert(new Toy("ball"));
+ ksession.fireAllRules();
+ ksession.insert(new Relationship("go", "done"));
+ ksession.fireAllRules();
+ assertThat(results).containsExactly("fired"); // fired once
+
+ // Restart: update resets the sequencer
+ ksession.update(anchorHandle, anchor);
+ ksession.fireAllRules();
+
+ // Second run: complete the sequence again
+ ksession.insert(new Toy("ball"));
+ ksession.fireAllRules();
+ ksession.insert(new Relationship("go", "done"));
+ ksession.fireAllRules();
+
+ assertThat(results).hasSize(2); // fired a second time
+ }
+
+ @Test
+ public void retractStepEventDoesNotUndoStep() {
+ ksession = makeKSession();
+
+ ksession.insert(new Person("anchor"));
+ ksession.fireAllRules();
+
+ FactHandle toyHandle = ksession.insert(new Toy("ball"));
+ ksession.fireAllRules(); // step-1 consumed; sequence now at step-2
+
+ ksession.retract(toyHandle); // retract the step-1 fact — no-op for the sequencer
+ ksession.fireAllRules();
+
+ ksession.insert(new Relationship("go", "done"));
+ ksession.fireAllRules(); // step-2 arrives — sequence is still at step-2, fires
+
+ assertThat(results).containsExactly("fired");
+ }
+
+ @Test
+ public void updateStepEventDoesNotUndoStep() {
+ ksession = makeKSession();
+
+ ksession.insert(new Person("anchor"));
+ ksession.fireAllRules();
+
+ Toy toy = new Toy("ball");
+ FactHandle toyHandle = ksession.insert(toy);
+ ksession.fireAllRules(); // step-1 consumed; sequence now at step-2
+
+ ksession.update(toyHandle, toy); // update the step-1 fact — no-op for the sequencer
+ ksession.fireAllRules();
+
+ ksession.insert(new Relationship("go", "done"));
+ ksession.fireAllRules(); // step-2 arrives — sequence fires
+
+ assertThat(results).containsExactly("fired");
+ }
+
+ @Test
+ public void removedRuleNoLongerReceivesStepEvents() {
+ Variable<Person> personV = declarationOf(Person.class);
+ Variable<Toy> toyV = declarationOf(Toy.class);
+
+ Rule rule = rule("seq-remove-rule").build(
+ pattern(personV),
+ sequence(
+ pattern(toyV).expr("is-ball", t -> t.getName().equals("ball"))
+ ),
+ execute(() -> results.add("fired"))
+ );
+
+ Model model = new ModelImpl().addRule(rule);
+ KieBase kieBase = KieBaseBuilder.createKieBaseFromModel(model);
+ ksession = kieBase.newKieSession();
+
+ // Start the sequencer
+ ksession.insert(new Person("anchor"));
+ ksession.fireAllRules();
+
+ // Remove the rule from the KieBase while the session is active
+ kieBase.removeRule("defaultpkg", "seq-remove-rule");
+
+ // Insert the step event — the removed rule must NOT fire
+ ksession.insert(new Toy("ball"));
+ ksession.fireAllRules();
+
+ assertThat(results)
+ .as("no rule should fire after it has been removed from the KieBase")
+ .isEmpty();
+ }
+
+ private KieSession makeKSession() {
+ Rule rule = rule("lifecycle-rule").build(
+ pattern(person),
+ sequence(
+ pattern(toy).expr("is-ball", t -> t.getName().equals("ball")),
+ pattern(relationship).expr("is-go", r -> r.getStart().equals("go"))
+ ),
+ execute(() -> results.add("fired"))
+ );
+ Model model = new ModelImpl().addRule(rule);
+ KieBase kieBase = KieBaseBuilder.createKieBaseFromModel(model);
+ return kieBase.newKieSession();
+ }
+
+ private KieSession makeKSessionSingleStep() {
+ Rule rule = rule("lifecycle-single-step").build(
+ pattern(person),
+ sequence(
+ pattern(toy).expr("is-ball", t -> t.getName().equals("ball"))
+ ),
+ execute(() -> results.add("fired"))
+ );
+ Model model = new ModelImpl().addRule(rule);
+ KieBase kieBase = KieBaseBuilder.createKieBaseFromModel(model);
+ return kieBase.newKieSession();
+ }
+}
diff --git a/drools-model/drools-model-compiler/src/test/java/org/drools/modelcompiler/PatternDSLSequenceTest.java b/drools-model/drools-model-compiler/src/test/java/org/drools/modelcompiler/PatternDSLSequenceTest.java
new file mode 100644
index 00000000000..dae95a6afab
--- /dev/null
+++ b/drools-model/drools-model-compiler/src/test/java/org/drools/modelcompiler/PatternDSLSequenceTest.java
@@ -0,0 +1,157 @@
+/*
+ * 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.modelcompiler;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.drools.model.Model;
+import org.drools.model.Rule;
+import org.drools.model.Variable;
+import org.drools.model.impl.ModelImpl;
+import org.drools.modelcompiler.domain.Person;
+import org.drools.modelcompiler.domain.Relationship;
+import org.drools.modelcompiler.domain.Toy;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+import org.kie.api.KieBase;
+import org.kie.api.runtime.KieSession;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.drools.model.DSL.declarationOf;
+import static org.drools.model.DSL.execute;
+import static org.drools.model.PatternDSL.pattern;
+import static org.drools.model.PatternDSL.rule;
+import static org.drools.model.PatternDSL.sequence;
+
+public class PatternDSLSequenceTest {
+
+ private final Variable<Person> person = declarationOf(Person.class);
+ private final Variable<Toy> toy = declarationOf(Toy.class);
+ private final Variable<Relationship> relationship = declarationOf(Relationship.class);
+ private final List<String> results = new ArrayList<>();
+ private KieSession ksession;
+
+ private final Rule rule =
+ rule("seq-rule").build(
+ pattern(person),
+ sequence(
+ pattern(toy).expr("toy-filter",
+ t -> t.getName().equals("ball")),
+ pattern(relationship).expr("rel-filter",
+ r -> r.getStart().equals("go"))
+ ),
+ execute(() -> results.add("fired"))
+ );
+
+ @Test
+ public void sequenceFiresWhenToyThenRelationship() {
+ ksession = makeKSession();
+
+ // LHS anchor — activates the sequencer
+ insertAndFire(
+ new Person("anchor")
+ );
+
+ // Step 1, then step 2 — rule should fire
+ insertAndFire(
+ new Toy("ball"),
+ new Relationship("go", "done")
+ );
+
+ assertThat(results).containsExactly("fired");
+ }
+
+ @Test
+ public void sequenceFiresWithSeparateInserts() {
+ ksession = makeKSession();
+
+ // LHS anchor — activates the sequencer
+ insertAndFire(
+ new Person("anchor")
+ );
+
+ // Step 1, then fireAll() — rule should NOT fire
+ insertAndFire(
+ new Toy("ball")
+ );
+
+ assertThat(results).isEmpty();
+
+ // Then step 2 — rule should fire
+ insertAndFire(
+ new Relationship("go", "done")
+ );
+
+ assertThat(results).containsExactly("fired");
+ }
+
+ @Test
+ public void sequenceDoesNotFireWithoutCorrectOrder() {
+ ksession = makeKSession();
+
+ // LHS anchor — activates the sequencer
+ insertAndFire(
+ new Person("anchor")
+ );
+
+ // Step 1, then step 2 — sequence expects Toy then Relationship. We provide Relationship then Toy
+ insertAndFire(
+ new Relationship("go", "done"),
+ new Toy("ball"));
+
+ assertThat(results).isEmpty();
+ }
+
+ @Test
+ public void sequenceDoesNotFireWithoutToy() {
+ ksession = makeKSession();
+
+ insertAndFire(
+ new Person("anchor")
+ );
+
+ // Step 2 arrives without step 1 — rule must NOT fire
+ insertAndFire(
+ new Relationship("go", "done")
+ );
+
+ assertThat(results).isEmpty();
+ }
+
+ @AfterEach
+ public void tearDown() {
+ results.clear();
+ ksession.dispose();
+ }
+
+ private void insertAndFire(Object... facts) {
+ for (Object fact : facts) {
+ ksession.insert(fact);
+ }
+ ksession.fireAllRules();
+ }
+
+ private KieSession makeKSession() {
+ final Model model = new ModelImpl().addRule(rule);
+ final KieBase kieBase = KieBaseBuilder.createKieBaseFromModel(model);
+ return kieBase.newKieSession();
+ }
+
+}
diff --git a/drools-model/drools-model-compiler/src/test/java/org/drools/modelcompiler/SequenceReviewProbeTest.java b/drools-model/drools-model-compiler/src/test/java/org/drools/modelcompiler/SequenceReviewProbeTest.java
new file mode 100644
index 00000000000..091f2a6b6b6
--- /dev/null
+++ b/drools-model/drools-model-compiler/src/test/java/org/drools/modelcompiler/SequenceReviewProbeTest.java
@@ -0,0 +1,288 @@
+/*
+ * 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.modelcompiler;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.drools.kiesession.rulebase.InternalKnowledgeBase;
+import org.drools.model.Model;
+import org.drools.model.Rule;
+import org.drools.model.Variable;
+import org.drools.model.impl.ModelImpl;
+import org.drools.modelcompiler.domain.Person;
+import org.drools.modelcompiler.domain.Toy;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+import org.kie.api.KieBase;
+import org.kie.api.event.rule.DefaultAgendaEventListener;
+import org.kie.api.event.rule.MatchCreatedEvent;
+import org.kie.api.runtime.KieSession;
+import org.kie.api.runtime.KieSessionsPool;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.drools.model.DSL.declarationOf;
+import static org.drools.model.DSL.execute;
+import static org.drools.model.PatternDSL.pattern;
+import static org.drools.model.PatternDSL.rule;
+import static org.drools.model.PatternDSL.sequence;
+
+public class SequenceReviewProbeTest {
+
+ private final List<String> results = new ArrayList<>();
+ private KieSession ksession;
+
+ @AfterEach
+ public void tearDown() {
+ results.clear();
+ if (ksession != null) {
+ ksession.dispose();
+ }
+ }
+
+ @Test
+ public void plainRuleAndSequenceRuleShareAnEventType() {
+ Variable<Toy> toy = declarationOf(Toy.class);
+ Variable<Person> person = declarationOf(Person.class);
+
+ Rule plainRule = rule("plain-rule").build(
+ pattern(toy).expr("is-ball", t -> t.getName().equals("ball")),
+ execute(() -> results.add("plain:ball"))
+ );
+
+ Rule seqRule = rule("seq-rule").build(
+ pattern(person),
+ sequence(
+ pattern(toy).expr("seq-is-ball", t -> t.getName().equals("ball"))
+ ),
+ execute(() -> results.add("seq:ball"))
+ );
+
+ Model model = new ModelImpl().addRule(plainRule).addRule(seqRule);
+ KieBase kieBase = KieBaseBuilder.createKieBaseFromModel(model);
+ ksession = kieBase.newKieSession();
+
+ ksession.insert(new Person("anchor"));
+ ksession.fireAllRules();
+
+ ksession.insert(new Toy("ball"));
+ ksession.fireAllRules();
+
+ // Both the plain rule and the sequence rule must fire.
+ assertThat(results).contains("plain:ball");
+ assertThat(results).contains("seq:ball");
+ }
+
+ @Test
+ public void sameTypeInTwoSteps() {
+ Variable<Toy> toy = declarationOf(Toy.class);
+ Variable<Person> person = declarationOf(Person.class);
+
+ Rule rule = rule("ball-then-doll").build(
+ pattern(person),
+ sequence(
+ pattern(toy).expr("is-ball", t -> t.getName().equals("ball")),
+ pattern(toy).expr("is-doll", t -> t.getName().equals("doll"))
+ ),
+ execute(() -> results.add("fired"))
+ );
+
+ ksession = KieBaseBuilder.createKieBaseFromModel(
+ new org.drools.model.impl.ModelImpl().addRule(rule))
+ .newKieSession();
+
+ ksession.insert(new Person("anchor"));
+ ksession.fireAllRules();
+
+ ksession.insert(new Toy("ball"));
+ ksession.fireAllRules();
+
+ ksession.insert(new Toy("doll"));
+ ksession.fireAllRules();
+
+ assertThat(results).containsExactly("fired");
+ }
+
+ @Test
+ public void stepWithTwoConstraintsIsUnsatisfiable() {
+ Variable<Toy> toy = declarationOf(Toy.class);
+ Variable<Person> person = declarationOf(Person.class);
+
+ // A step that requires name == "ball" AND name == "doll" simultaneously —
+ // logically impossible, must never match any real Toy.
+ Rule rule = rule("impossible-step").build(
+ pattern(person),
+ sequence(
+ pattern(toy)
+ .expr("is-ball", t -> t.getName().equals("ball"))
+ .expr("is-doll", t -> t.getName().equals("doll"))
+ ),
+ execute(() -> results.add("fired"))
+ );
+
+ ksession = KieBaseBuilder.createKieBaseFromModel(
+ new org.drools.model.impl.ModelImpl().addRule(rule))
+ .newKieSession();
+
+ ksession.insert(new Person("anchor"));
+ ksession.fireAllRules();
+
+ // A Toy("ball") satisfies "is-ball" but not "is-doll" — must NOT fire.
+ ksession.insert(new Toy("ball"));
+ ksession.fireAllRules();
+
+ assertThat(results).isEmpty();
+ }
+
+ @Test
+ public void stepWithTwoConstraintsBothSatisfiedFires() {
+ Variable<Toy> toy = declarationOf(Toy.class);
+ Variable<Person> person = declarationOf(Person.class);
+
+ // A step that requires name starts with "t" AND length > 3.
+ // Toy("toy-car") satisfies both — CombinedAlphaConstraint.isAllowed()
+ // must return true (the all-pass branch at the end of the loop).
+ Rule rule = rule("two-constraint-step").build(
+ pattern(person),
+ sequence(
+ pattern(toy)
+ .expr("starts-with-t", t -> t.getName().startsWith("t"))
+ .expr("longer-than-3", t -> t.getName().length() > 3)
+ ),
+ execute(() -> results.add("fired"))
+ );
+
+ ksession = KieBaseBuilder.createKieBaseFromModel(
+ new org.drools.model.impl.ModelImpl().addRule(rule))
+ .newKieSession();
+
+ ksession.insert(new Person("anchor"));
+ ksession.fireAllRules();
+
+ // Toy("toy-car"): starts with "t" ✓, length 7 > 3 ✓ — both constraints pass.
+ ksession.insert(new Toy("toy-car"));
+ ksession.fireAllRules();
+
+ assertThat(results).containsExactly("fired");
+ }
+
+ @Test
+ public void joinAfterSequenceHasCorrectTupleWidth() {
+ Variable<Person> anchorV = declarationOf(Person.class);
+ Variable<Toy> toyV = declarationOf(Toy.class);
+ Variable<Person> observerV = declarationOf(Person.class);
+
+ Rule rule = rule("join-after-sequence").build(
+ pattern(anchorV).expr("is-anchor", p -> p.getName().equals("anchor")),
+ sequence(
+ pattern(toyV).expr("is-ball", t -> t.getName().equals("ball"))
+ ),
+ pattern(observerV).expr("is-observer", p -> p.getName().equals("observer")),
+ execute(() -> results.add("fired"))
+ );
+
+ KieBase kbase = KieBaseBuilder.createKieBaseFromModel(new ModelImpl().addRule(rule));
+ ksession = kbase.newKieSession();
+
+ AtomicInteger capturedHandleCount = new AtomicInteger(-1);
+ ksession.addEventListener(new DefaultAgendaEventListener() {
+ @Override
+ public void matchCreated(MatchCreatedEvent event) {
+ capturedHandleCount.set(event.getMatch().getFactHandles().size());
+ }
+ });
+
+ ksession.insert(new Person("anchor"));
+ ksession.insert(new Person("observer"));
+ ksession.fireAllRules();
+
+ ksession.insert(new Toy("ball"));
+ ksession.fireAllRules();
+
+ assertThat(results).containsExactly("fired");
+ assertThat(capturedHandleCount.get())
+ .as("match must contain both anchor and observer fact handles")
+ .isEqualTo(2);
+ }
+
+ @Test
+ public void stepWithNoConstraintThrowsDescriptiveError() {
+ Variable<Toy> toy = declarationOf(Toy.class);
+ Variable<Person> person = declarationOf(Person.class);
+
+ Rule rule = rule("unconstrained-step").build(
+ pattern(person),
+ sequence(
+ pattern(toy)
+ ),
+ execute(() -> results.add("fired"))
+ );
+
+ assertThat(org.assertj.core.api.Assertions.catchThrowableOfType(() ->
+ KieBaseBuilder.createKieBaseFromModel(
+ new org.drools.model.impl.ModelImpl().addRule(rule)),
+ Exception.class))
+ .as("step with no constraint must throw a descriptive build error")
+ .isNotNull()
+ .isNotInstanceOf(IndexOutOfBoundsException.class)
+ .hasMessageContaining("sequence step");
+ }
+
+ @Test
+ public void pooledSessionReuseShouldNotLeakSequenceState() {
+ Variable<Toy> toy = declarationOf(Toy.class);
+ Variable<Person> person = declarationOf(Person.class);
+
+ Rule rule = rule("seq-rule").build(
+ pattern(person),
+ sequence(
+ pattern(toy).expr("is-ball", t -> t.getName().equals("ball"))
+ ),
+ execute(() -> results.add("fired"))
+ );
+
+ InternalKnowledgeBase kbase = KieBaseBuilder.createKieBaseFromModel(
+ new ModelImpl().addRule(rule));
+ KieSessionsPool pool = kbase.newKieSessionsPool(1);
+
+ KieSession s1 = pool.newKieSession();
+ s1.insert(new Person("anchor"));
+ s1.fireAllRules();
+ // Do NOT complete the sequence — dispose mid-flight.
+ s1.dispose();
+
+ KieSession s2 = pool.newKieSession();
+ assertThat(s2).as("pool of 1 must return the same physical session").isSameAs(s1);
+
+ s2.insert(new Person("anchor"));
+ s2.fireAllRules();
+ s2.insert(new Toy("ball"));
+ s2.fireAllRules();
+ int secondFires = results.size();
+
+ pool.shutdown();
+
+ assertThat(secondFires)
+ .as("sequence must fire exactly once in session #2 — " +
+ "stale DynamicFilter/SignalAdapter state from session #1 must not cause extra or missing fires")
+ .isEqualTo(1);
+ }
+
+}
diff --git a/drools-model/drools-model-compiler/src/test/java/org/drools/modelcompiler/domain/SensorEvents.java b/drools-model/drools-model-compiler/src/test/java/org/drools/modelcompiler/domain/SensorEvents.java
new file mode 100644
index 00000000000..cac2b2dfa18
--- /dev/null
+++ b/drools-model/drools-model-compiler/src/test/java/org/drools/modelcompiler/domain/SensorEvents.java
@@ -0,0 +1,120 @@
+/*
+ * 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.modelcompiler.domain;
+
+public final class SensorEvents {
+
+ private SensorEvents() { }
+
+ public static final class MonitoringStation {
+ private final String id;
+
+ public MonitoringStation(String id) {
+ this.id = id;
+ }
+
+ public String getId() {
+ return id;
+ }
+ }
+
+ public static final class SensorActivated {
+ private final String sensorId;
+
+ public SensorActivated(String sensorId) {
+ this.sensorId = sensorId;
+ }
+
+ public String getSensorId() {
+ return sensorId;
+ }
+ }
+
+ public static final class HeartbeatOk {
+ private final String sensorId;
+
+ public HeartbeatOk(String sensorId) {
+ this.sensorId = sensorId;
+ }
+
+ public String getSensorId() {
+ return sensorId;
+ }
+ }
+
+ public static final class AlarmRaised {
+ private final String sensorId;
+ private final String severity;
+
+ public AlarmRaised(String sensorId, String severity) {
+ this.sensorId = sensorId;
+ this.severity = severity;
+ }
+
+ public String getSensorId() {
+ return sensorId;
+ }
+
+ public String getSeverity() {
+ return severity;
+ }
+ }
+
+ public static final class CalibrationPassed {
+ private final String sensorId;
+
+ public CalibrationPassed(String sensorId) {
+ this.sensorId = sensorId;
+ }
+
+ public String getSensorId() {
+ return sensorId;
+ }
+ }
+
+ public static final class MaintenanceScheduled {
+ private final String sensorId;
+
+ public MaintenanceScheduled(String sensorId) {
+ this.sensorId = sensorId;
+ }
+
+ public String getSensorId() {
+ return sensorId;
+ }
+ }
+
+ public static final class OperatorAcknowledged {
+ private final String sensorId;
+ private final String operator;
+
+ public OperatorAcknowledged(String sensorId, String operator) {
+ this.sensorId = sensorId;
+ this.operator = operator;
+ }
+
+ public String getSensorId() {
+ return sensorId;
+ }
+
+ public String getOperator() {
+ return operator;
+ }
+ }
+}
diff --git a/drools-ruleunits/drools-ruleunits-impl/src/main/java/org/drools/ruleunits/impl/sessions/RuleUnitExecutorImpl.java b/drools-ruleunits/drools-ruleunits-impl/src/main/java/org/drools/ruleunits/impl/sessions/RuleUnitExecutorImpl.java
index 72308a32138..47c8d9f7ec7 100644
--- a/drools-ruleunits/drools-ruleunits-impl/src/main/java/org/drools/ruleunits/impl/sessions/RuleUnitExecutorImpl.java
+++ b/drools-ruleunits/drools-ruleunits-impl/src/main/java/org/drools/ruleunits/impl/sessions/RuleUnitExecutorImpl.java
@@ -22,6 +22,7 @@ import org.drools.base.beliefsystem.Mode;
import org.drools.base.definitions.rule.impl.RuleImpl;
import org.drools.base.factmodel.traits.Thing;
import org.drools.base.factmodel.traits.TraitableBean;
+import org.drools.base.phreak.PropagationEntry;
import org.drools.base.rule.Declaration;
import org.drools.base.rule.accessor.GlobalResolver;
import org.drools.core.EntryPointsManager;
@@ -53,7 +54,6 @@ import org.drools.core.event.RuleEventListenerSupport;
import org.drools.core.event.RuleRuntimeEventSupport;
import org.drools.core.impl.ActivationsManagerImpl;
import org.drools.core.impl.InternalRuleBase;
-import org.drools.base.phreak.PropagationEntry;
import org.drools.core.phreak.actions.ExecuteQuery;
import org.drools.core.phreak.RuleNetworkEvaluator;
import org.drools.core.phreak.RuleNetworkEvaluatorImpl;
diff --git a/drools-test-coverage/test-compiler-integration/src/test/java/org/drools/mvel/integrationtests/phreak/sequencing/AbstractPhreakSequencerSubsequenceTest.java b/drools-test-coverage/test-compiler-integration/src/test/java/org/drools/mvel/integrationtests/phreak/sequencing/AbstractPhreakSequencerSubsequenceTest.java
new file mode 100644
index 00000000000..e234210ddd7
--- /dev/null
+++ b/drools-test-coverage/test-compiler-integration/src/test/java/org/drools/mvel/integrationtests/phreak/sequencing/AbstractPhreakSequencerSubsequenceTest.java
@@ -0,0 +1,183 @@
+/*
+ * 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.phreak.sequencing;
+
+import org.drools.base.base.ClassObjectType;
+import org.drools.base.base.ObjectType;
+import org.drools.base.base.ValueResolver;
+import org.drools.base.definitions.InternalKnowledgePackage;
+import org.drools.base.definitions.rule.impl.RuleImpl;
+import org.drools.base.reteoo.sequencing.Sequence.SequenceMemory;
+import org.drools.base.rule.EntryPointId;
+import org.drools.base.rule.Pattern;
+import org.drools.base.rule.consequence.Consequence;
+import org.drools.base.rule.consequence.ConsequenceContext;
+import org.drools.core.ClockType;
+import org.drools.core.SessionConfiguration;
+import org.drools.core.common.InternalFactHandle;
+import org.drools.core.impl.KnowledgeBaseImpl;
+import org.drools.core.impl.RuleBaseFactory;
+import org.drools.core.reteoo.CoreComponentFactory;
+import org.drools.core.reteoo.ObjectTypeNode;
+import org.drools.core.reteoo.PathMemory;
+import org.drools.core.reteoo.SequenceNode;
+import org.drools.core.reteoo.SequenceNode.SequenceNodeMemory;
+import org.drools.core.reteoo.builder.BuildContext;
+import org.drools.base.reteoo.sequencing.Sequence;
+import org.drools.base.reteoo.sequencing.SequencerMemory;
+import org.drools.core.rule.JavaDialectRuntimeData;
+import org.drools.kiesession.rulebase.SessionsAwareKnowledgeBase;
+import org.drools.kiesession.session.StatefulKnowledgeSessionImpl;
+import org.drools.mvel.integrationtests.phreak.A;
+import org.kie.api.conf.EventProcessingOption;
+import org.kie.api.runtime.conf.ThreadSafeOption;
+import org.kie.internal.conf.CompositeBaseConfiguration;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+public class AbstractPhreakSequencerSubsequenceTest {
+ StatefulKnowledgeSessionImpl session;
+ SequenceNodeMemory nodeMemory;
+ PathMemory pmem;
+ SequencerMemory sequencerMemory;
+ InternalFactHandle fhA0;
+ BuildContext buildContext;
+ Sequence seq0;
+ SequenceNode snode;
+
+ Pattern bpattern;
+ Pattern cpattern;
+ Pattern dpattern;
+ Pattern epattern;
+
+ RuleImpl rule;
+ InternalKnowledgePackage pkg;
+ SessionsAwareKnowledgeBase kbase;
+
+ public void initKBaseWithEmptyRule() {
+ CompositeBaseConfiguration conf = (CompositeBaseConfiguration) RuleBaseFactory.newKnowledgeBaseConfiguration();
+ conf.setOption(EventProcessingOption.STREAM);
+
+ KnowledgeBaseImpl rbase = new KnowledgeBaseImpl("ID", conf );
+
+ kbase = new SessionsAwareKnowledgeBase(rbase);
+
+ rule = new RuleImpl("rule1").setPackage("org.pkg1");
+ rule.setConsequence(new Consequence() {
+ @Override
+ public String getName() {
+ return "r1";
+ }
+
+ @Override
+ public void evaluate(ConsequenceContext knowledgeHelper, ValueResolver valueResolver) throws Exception {
+ }
+ });
+ pkg = CoreComponentFactory.get().createKnowledgePackage("org.pkg1");
+ pkg.getDialectRuntimeRegistry().setDialectData( "java", new JavaDialectRuntimeData());
+ pkg.addRule( rule );
+
+ final ObjectType aObjectType = new ClassObjectType(A.class);
+ final ObjectType bObjectType = new ClassObjectType(BEvent.class);
+ final ObjectType cObjectType = new ClassObjectType(CEvent.class);
+ final ObjectType dObjectType = new ClassObjectType(DEvent.class);
+ final ObjectType eObjectType = new ClassObjectType(EEvent.class);
+
+ Pattern aPattern = new Pattern(0, aObjectType);
+ rule.addPattern(aPattern);
+
+ bpattern = new Pattern(0, bObjectType, "b" );
+ bpattern.addConstraint(new AlphaConstraint((Predicate1<BEvent>) b -> b.getText().equals("b")));
+
+ cpattern = new Pattern(0, cObjectType, "c" );
+ cpattern.addConstraint(new AlphaConstraint( (Predicate1<CEvent>) c -> c.getText().equals("c")));
+
+ dpattern = new Pattern(0, dObjectType, "d" );
+ dpattern.addConstraint(new AlphaConstraint( (Predicate1<DEvent>) d -> d.getText().equals("d")));
+
+ epattern = new Pattern(0, eObjectType, "e" );
+ epattern.addConstraint(new AlphaConstraint( (Predicate1<EEvent>) e -> e.getText().equals("e")));
+ }
+
+ public static BuildContext createContext() {
+
+ CompositeBaseConfiguration conf = (CompositeBaseConfiguration) RuleBaseFactory.newKnowledgeBaseConfiguration();
+
+ KnowledgeBaseImpl rbase = new KnowledgeBaseImpl("ID",
+ conf );
+ BuildContext buildContext = new BuildContext(rbase, Collections.emptyList() );
+
+ RuleImpl rule = new RuleImpl("rule1").setPackage("org.pkg1");
+ InternalKnowledgePackage pkg = CoreComponentFactory.get().createKnowledgePackage("org.pkg1");
+ pkg.getDialectRuntimeRegistry().setDialectData( "java", new JavaDialectRuntimeData());
+
+ pkg.addRule( rule );
+ buildContext.setRule( rule );
+
+ return buildContext;
+ }
+
+ void createSession() {
+ SessionConfiguration sessionConf = kbase.getSessionConfiguration();
+ sessionConf.setOption(ThreadSafeOption.NO);
+ sessionConf.setClockType(ClockType.PSEUDO_CLOCK);
+
+ if (snode == null) {
+ ObjectTypeNode aNode = kbase.getRete().getEntryPointNode(EntryPointId.DEFAULT).getObjectTypeNodes().get(new ClassObjectType(A.class));
+ snode = (SequenceNode) aNode.getSinks()[0].getSinks()[0];
+ }
+
+ if (session != null) {
+ nodeMemory = null;
+ sequencerMemory = null;
+ session.dispose();
+ session = null;
+ }
+
+ session = (StatefulKnowledgeSessionImpl) kbase.newKieSession(sessionConf, null);
+
+ fhA0 = (InternalFactHandle) session.insert(new A(0));
+ session.fireAllRules();
+ nodeMemory = session.getNodeMemory(snode);
+ pmem = nodeMemory.getSegmentMemory().getPathMemories().get(0);
+ sequencerMemory = (SequencerMemory) fhA0.getFirstLeftTuple().getContextObject();
+ }
+
+ public int getCurrentStep(SequencerMemory sqncrMemory) {
+ SequenceMemory sqncMemory = sqncrMemory.getChildSequenceMemory();
+
+ List<SequenceMemory> leafSequences = new ArrayList<>();
+ getLeafSequences(sqncrMemory, sqncMemory, leafSequences);
+
+ if (leafSequences.isEmpty()) {
+ return -1;
+ }
+ return leafSequences.get(0).getStep();
+ }
+
+ public void getLeafSequences(SequencerMemory sqncrMemory, SequenceMemory sqncMemory, List<SequenceMemory> leafSequences) {
+ Sequence sqnc = sqncMemory.getSequence();
+ if (sqncMemory.getStep() >= sqnc.getSteps().length ) {
+ return;
+ }
+ leafSequences.add(sqncMemory);
+ }
+}
diff --git a/drools-test-coverage/test-compiler-integration/src/test/java/org/drools/mvel/integrationtests/phreak/sequencing/AlphaConstraint.java b/drools-test-coverage/test-compiler-integration/src/test/java/org/drools/mvel/integrationtests/phreak/sequencing/AlphaConstraint.java
new file mode 100644
index 00000000000..7139a937064
--- /dev/null
+++ b/drools-test-coverage/test-compiler-integration/src/test/java/org/drools/mvel/integrationtests/phreak/sequencing/AlphaConstraint.java
@@ -0,0 +1,73 @@
+/*
+ * 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.phreak.sequencing;
+
+import org.drools.base.base.ValueResolver;
+import org.drools.base.reteoo.BaseTuple;
+import org.drools.base.rule.Declaration;
+import org.drools.base.rule.MutableTypeConstraint;
+import org.kie.api.runtime.rule.FactHandle;
+
+public class AlphaConstraint extends MutableTypeConstraint {
+ private Predicate1 predicate1;
+
+ public AlphaConstraint(Predicate1 predicate1) {
+ this.predicate1 = predicate1;
+ }
+
+ @Override
+ public Declaration[] getRequiredDeclarations() {
+ return new Declaration[0];
+ }
+
+ @Override
+ public void replaceDeclaration(Declaration oldDecl, Declaration newDecl) {
+
+ }
+
+ @Override
+ public MutableTypeConstraint clone() {
+ return new AlphaConstraint(predicate1);
+ }
+
+ @Override
+ public boolean isTemporal() {
+ return false;
+ }
+
+ @Override
+ public boolean isAllowed(FactHandle handle, ValueResolver valueResolver) {
+ return predicate1.test(handle.getObject());
+ }
+
+ @Override
+ public boolean isAllowedCachedLeft(Object context, FactHandle handle) {
+ throw new UnsupportedOperationException("AlphaConstraint does not support cached context evaluation");
+ }
+
+ @Override
+ public boolean isAllowedCachedRight(BaseTuple tuple, Object context) {
+ throw new UnsupportedOperationException("AlphaConstraint does not support cached context evaluation");
+ }
+
+ @Override
+ public Object createContext() {
+ throw new UnsupportedOperationException("AlphaConstraint does not support cached context evaluation");
+ }
+}
diff --git a/drools-model/drools-canonical-model/src/main/java/org/drools/model/view/ExistentialExprViewItem.java b/drools-test-coverage/test-compiler-integration/src/test/java/org/drools/mvel/integrationtests/phreak/sequencing/BEvent.java
similarity index 55%
copy from drools-model/drools-canonical-model/src/main/java/org/drools/model/view/ExistentialExprViewItem.java
copy to drools-test-coverage/test-compiler-integration/src/test/java/org/drools/mvel/integrationtests/phreak/sequencing/BEvent.java
index d74396d1070..49aede24dec 100644
--- a/drools-model/drools-canonical-model/src/main/java/org/drools/model/view/ExistentialExprViewItem.java
+++ b/drools-test-coverage/test-compiler-integration/src/test/java/org/drools/mvel/integrationtests/phreak/sequencing/BEvent.java
@@ -16,33 +16,30 @@
* specific language governing permissions and limitations
* under the License.
*/
-package org.drools.model.view;
+package org.drools.mvel.integrationtests.phreak.sequencing;
-import org.drools.model.Condition;
-import org.drools.model.Variable;
+import org.drools.mvel.integrationtests.phreak.B;
+import org.kie.api.definition.type.Position;
-public class ExistentialExprViewItem<T> extends AbstractExprViewItem<T> {
+public class BEvent extends B {
- private final Condition.Type type;
- private final ViewItem expression;
+ @Position(1)
+ private String text;
- public ExistentialExprViewItem(Condition.Type type, ViewItem expression) {
- super(expression.getFirstVariable());
- this.type = type;
- this.expression = expression;
+ public BEvent(Object object) {
+ super(object);
}
- public ViewItem getExpression() {
- return expression;
+ public BEvent(Object object, String text) {
+ super(object);
+ this.text = text;
}
- @Override
- public Variable<?>[] getVariables() {
- return expression.getVariables();
+ public String getText() {
+ return text;
}
- @Override
- public Condition.Type getType() {
- return type;
+ public void setText(String text) {
+ this.text = text;
}
}
diff --git a/drools-model/drools-canonical-model/src/main/java/org/drools/model/view/ExistentialExprViewItem.java b/drools-test-coverage/test-compiler-integration/src/test/java/org/drools/mvel/integrationtests/phreak/sequencing/CEvent.java
similarity index 55%
copy from drools-model/drools-canonical-model/src/main/java/org/drools/model/view/ExistentialExprViewItem.java
copy to drools-test-coverage/test-compiler-integration/src/test/java/org/drools/mvel/integrationtests/phreak/sequencing/CEvent.java
index d74396d1070..96b46df04d7 100644
--- a/drools-model/drools-canonical-model/src/main/java/org/drools/model/view/ExistentialExprViewItem.java
+++ b/drools-test-coverage/test-compiler-integration/src/test/java/org/drools/mvel/integrationtests/phreak/sequencing/CEvent.java
@@ -16,33 +16,30 @@
* specific language governing permissions and limitations
* under the License.
*/
-package org.drools.model.view;
+package org.drools.mvel.integrationtests.phreak.sequencing;
-import org.drools.model.Condition;
-import org.drools.model.Variable;
+import org.drools.mvel.integrationtests.phreak.C;
+import org.kie.api.definition.type.Position;
-public class ExistentialExprViewItem<T> extends AbstractExprViewItem<T> {
+public class CEvent extends C {
- private final Condition.Type type;
- private final ViewItem expression;
+ @Position(1)
+ private String text;
- public ExistentialExprViewItem(Condition.Type type, ViewItem expression) {
- super(expression.getFirstVariable());
- this.type = type;
- this.expression = expression;
+ public CEvent(Object object) {
+ super(object);
}
- public ViewItem getExpression() {
- return expression;
+ public CEvent(Object object, String text) {
+ super(object);
+ this.text = text;
}
- @Override
- public Variable<?>[] getVariables() {
- return expression.getVariables();
+ public String getText() {
+ return text;
}
- @Override
- public Condition.Type getType() {
- return type;
+ public void setText(String text) {
+ this.text = text;
}
}
diff --git a/drools-test-coverage/test-compiler-integration/src/test/java/org/drools/mvel/integrationtests/phreak/sequencing/CircularArrayListTest.java b/drools-test-coverage/test-compiler-integration/src/test/java/org/drools/mvel/integrationtests/phreak/sequencing/CircularArrayListTest.java
new file mode 100644
index 00000000000..4e6d1bb46cc
--- /dev/null
+++ b/drools-test-coverage/test-compiler-integration/src/test/java/org/drools/mvel/integrationtests/phreak/sequencing/CircularArrayListTest.java
@@ -0,0 +1,91 @@
+/*
+ * 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.phreak.sequencing;
+
+import org.drools.base.util.CircularArrayList;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+public class CircularArrayListTest {
+
+ @Test
+ public void testAddingWithWrap() {
+ CircularArrayList<Integer> list = new CircularArrayList<>(10);
+
+ for (int i = 0; i < 15; i++) {
+ list.add(i);
+ }
+
+ assertThat(list.size()).isEqualTo(15);
+ assertThat(list.get(list.size()-1)).isEqualTo(14);
+ assertThat(list.get(14)).isEqualTo(14);
+ assertThat(list.getHead()).isEqualTo(14);
+ assertThat(list.getHeadMinus(3)).isEqualTo(11);
+
+ assertThatThrownBy(() -> list.get(-1)).isInstanceOf(IndexOutOfBoundsException.class);
+ assertThatThrownBy(() -> list.get(0)).isInstanceOf(IndexOutOfBoundsException.class);
+ assertThatThrownBy(() -> list.get(4)).isInstanceOf(IndexOutOfBoundsException.class);
+ assertThat(list.get(5)).isEqualTo(5);
+ }
+
+ @Test
+ public void testNegativeIndexBeforeBufferFills() {
+ // When head < windowSize, head - windowSize is negative.
+ // A negative index must still be rejected even though it satisfies
+ // index >= (head - windowSize) when that difference is negative.
+ CircularArrayList<Integer> list = new CircularArrayList<>(10);
+ list.add(0);
+ list.add(1);
+ list.add(2);
+
+ assertThatThrownBy(() -> list.get(-1)).isInstanceOf(IndexOutOfBoundsException.class);
+ assertThatThrownBy(() -> list.get(-7)).isInstanceOf(IndexOutOfBoundsException.class);
+ }
+
+ @Test
+ public void testToArray() {
+ CircularArrayList<Object> list = new CircularArrayList<>(10);
+ assertThat(list.toArray().length).isEqualTo(0);
+ list.add(0);
+ assertThat(list.toArray()).isEqualTo(new Object[] {0});
+ list.add(1);
+ assertThat(list.toArray()).isEqualTo(new Object[] {0, 1});
+ list.add(2);
+ list.add(3);
+ list.add(4);
+ list.add(5);
+ list.add(6);
+ list.add(7);
+ list.add(8);
+ assertThat(list.toArray()).isEqualTo(new Object[] {0, 1, 2, 3, 4, 5, 6, 7, 8});
+ list.add(9);
+ assertThat(list.toArray()).isEqualTo(new Object[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9});
+ list.add(10);
+ assertThat(list.toArray()).isEqualTo(new Object[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 10});
+ list.add(11);
+ list.add(12);
+ list.add(13);
+ list.add(14);
+ list.add(15);
+ assertThat(list.toArray()).isEqualTo(new Object[] {6, 7, 8, 9, 10, 11, 12, 13, 14, 15});
+ }
+
+}
diff --git a/drools-test-coverage/test-compiler-integration/src/test/java/org/drools/mvel/integrationtests/phreak/sequencing/DEvent.java b/drools-test-coverage/test-compiler-integration/src/test/java/org/drools/mvel/integrationtests/phreak/sequencing/DEvent.java
new file mode 100644
index 00000000000..34905d6b02f
--- /dev/null
+++ b/drools-test-coverage/test-compiler-integration/src/test/java/org/drools/mvel/integrationtests/phreak/sequencing/DEvent.java
@@ -0,0 +1,80 @@
+/*
+ * 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.phreak.sequencing;
+
+import org.kie.api.definition.type.Position;
+
+public class DEvent {
+
+ @Position(0)
+ private Object object;
+
+ @Position(1)
+ private String text;
+
+ public DEvent(Object object) {
+ this.object = object;
+ }
+
+ public DEvent(Object object, String text) {
+ this.object = object;
+ this.text = text;
+ }
+
+ public Object getObject() {
+ return object;
+ }
+
+ public void setObject(Object object) {
+ this.object = object;
+ }
+
+ public String getText() {
+ return text;
+ }
+
+ public void setText(String text) {
+ this.text = text;
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + ((object == null) ? 0 : object.hashCode());
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if ( this == obj ) return true;
+ if ( obj == null ) return false;
+ if ( getClass() != obj.getClass() ) return false;
+ DEvent other = (DEvent) obj;
+ if ( object == null ) {
+ if ( other.object != null ) return false;
+ } else if ( !object.equals( other.object ) ) return false;
+ return true;
+ }
+
+ @Override
+ public String toString() {
+ return "D [" + object + "]";
+ }
+}
diff --git a/drools-model/drools-canonical-model/src/main/java/org/drools/model/view/ExistentialExprViewItem.java b/drools-test-coverage/test-compiler-integration/src/test/java/org/drools/mvel/integrationtests/phreak/sequencing/EEvent.java
similarity index 55%
copy from drools-model/drools-canonical-model/src/main/java/org/drools/model/view/ExistentialExprViewItem.java
copy to drools-test-coverage/test-compiler-integration/src/test/java/org/drools/mvel/integrationtests/phreak/sequencing/EEvent.java
index d74396d1070..3145f0ec630 100644
--- a/drools-model/drools-canonical-model/src/main/java/org/drools/model/view/ExistentialExprViewItem.java
+++ b/drools-test-coverage/test-compiler-integration/src/test/java/org/drools/mvel/integrationtests/phreak/sequencing/EEvent.java
@@ -16,33 +16,30 @@
* specific language governing permissions and limitations
* under the License.
*/
-package org.drools.model.view;
+package org.drools.mvel.integrationtests.phreak.sequencing;
-import org.drools.model.Condition;
-import org.drools.model.Variable;
+import org.drools.mvel.integrationtests.phreak.E;
+import org.kie.api.definition.type.Position;
-public class ExistentialExprViewItem<T> extends AbstractExprViewItem<T> {
+public class EEvent extends E {
- private final Condition.Type type;
- private final ViewItem expression;
+ @Position(1)
+ private String text;
- public ExistentialExprViewItem(Condition.Type type, ViewItem expression) {
- super(expression.getFirstVariable());
- this.type = type;
- this.expression = expression;
+ public EEvent(Object object) {
+ super(object);
}
- public ViewItem getExpression() {
- return expression;
+ public EEvent(Object object, String text) {
+ super(object);
+ this.text = text;
}
- @Override
- public Variable<?>[] getVariables() {
- return expression.getVariables();
+ public String getText() {
+ return text;
}
- @Override
- public Condition.Type getType() {
- return type;
+ public void setText(String text) {
+ this.text = text;
}
}
diff --git a/drools-test-coverage/test-compiler-integration/src/test/java/org/drools/mvel/integrationtests/phreak/sequencing/GatesTest.java b/drools-test-coverage/test-compiler-integration/src/test/java/org/drools/mvel/integrationtests/phreak/sequencing/GatesTest.java
new file mode 100644
index 00000000000..8d6fb31a57b
--- /dev/null
+++ b/drools-test-coverage/test-compiler-integration/src/test/java/org/drools/mvel/integrationtests/phreak/sequencing/GatesTest.java
@@ -0,0 +1,67 @@
+/*
+ * 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.phreak.sequencing;
+
+import org.drools.base.reteoo.sequencing.signalprocessors.Gates;
+import org.drools.base.reteoo.sequencing.signalprocessors.LogicGate;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+public class GatesTest {
+
+ // AND gate: matched when current == allMatched (all bits set)
+ @Test
+ public void testAndAllSet() {
+ assertThat(Gates.and(0b111L, 0b111L)).isTrue();
+ assertThat(Gates.and(0b110L, 0b111L)).isFalse();
+ }
+
+ // OR gate: matched when at least one bit of allMatched is set in current
+ @Test
+ public void testOrBasic() {
+ assertThat(Gates.or(0b001L, 0b111L)).isTrue();
+ assertThat(Gates.or(0b100L, 0b111L)).isTrue();
+ assertThat(Gates.or(0b000L, 0b111L)).isFalse();
+ }
+
+ @Test
+ public void testOrBit63() {
+ long bit63 = Long.MIN_VALUE; // 0x8000_0000_0000_0000 — only sign bit set
+ // OR gate: allMatched = bit63, current = bit63 → should match
+ assertThat(Gates.or(bit63, bit63)).isTrue();
+ }
+
+ @Test
+ public void testLogicGateTooManyInputsThrows() {
+ // Build an int[] of 65 filterIndexes/signalAdapterIndexes to exceed the limit.
+ int[] indexes = new int[65];
+ assertThatThrownBy(() -> new LogicGate((a, b) -> a == b, 0, indexes, indexes, 0))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("64");
+ }
+
+ @Test
+ public void testLogicGateExactly64InputsAllowed() {
+ int[] indexes = new int[32];
+ // 32 signal adapters + 32 input gates = 64 total — should not throw.
+ new LogicGate((a, b) -> a == b, 0, indexes, indexes, 32);
+ }
+}
diff --git a/drools-test-coverage/test-compiler-integration/src/test/java/org/drools/mvel/integrationtests/phreak/sequencing/PhreakSequencerActivateDeactivateStepTest.java b/drools-test-coverage/test-compiler-integration/src/test/java/org/drools/mvel/integrationtests/phreak/sequencing/PhreakSequencerActivateDeactivateStepTest.java
new file mode 100644
index 00000000000..9841f4741f6
--- /dev/null
+++ b/drools-test-coverage/test-compiler-integration/src/test/java/org/drools/mvel/integrationtests/phreak/sequencing/PhreakSequencerActivateDeactivateStepTest.java
@@ -0,0 +1,161 @@
+/**
+ * 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.phreak.sequencing;
+
+import org.drools.base.rule.Pattern;
+import org.drools.base.reteoo.DynamicFilter;
+import org.drools.base.reteoo.sequencing.Sequence.SequenceMemory;
+import org.drools.base.reteoo.SignalAdapter;
+import org.drools.base.reteoo.sequencing.signalprocessors.LogicCircuit;
+import org.drools.base.reteoo.sequencing.signalprocessors.Gates;
+import org.drools.base.reteoo.sequencing.signalprocessors.LogicGate;
+import org.drools.base.reteoo.sequencing.Sequence;
+import org.drools.base.reteoo.sequencing.steps.Step;
+import org.drools.base.reteoo.sequencing.signalprocessors.TerminatingSignalProcessor;
+import org.drools.mvel.integrationtests.phreak.A;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class PhreakSequencerActivateDeactivateStepTest extends AbstractPhreakSequencerSubsequenceTest {
+
+ private SequenceMemory sequenceMemory;
+
+ @BeforeEach
+ public void setup() {
+ initKBaseWithEmptyRule();
+
+ LogicGate gate1 = new LogicGate((inputMask, sourceMask) -> Gates.and(inputMask, sourceMask),0,
+ new int[] {0, 1}, // B and C
+ new int[] {0, 1}, // Each SignalAdapter must be in a unique index for the Sequence
+ 0);
+ gate1.setOutput(TerminatingSignalProcessor.get());
+
+ LogicCircuit circuit1 = new LogicCircuit(gate1);
+
+ LogicGate gate2 = new LogicGate((inputMask, sourceMask) -> Gates.and(inputMask, sourceMask),1,
+ new int[] {1, 2}, // C and D, C Filter is re-used
+ new int[] {2, 3}, // Each SignalAdapter must be in a unique index for the Sequence
+ 0);
+ gate2.setOutput(TerminatingSignalProcessor.get());
+
+ LogicCircuit circuit2 = new LogicCircuit(gate2);
+
+ seq0 = new Sequence(0, Step.of(circuit1), Step.of(circuit2));
+ seq0.setFilters(new Pattern[]{bpattern, cpattern, dpattern});
+ rule.addSequence(seq0);
+ kbase.addPackage(pkg);
+
+ createSession();
+ sequenceMemory = sequencerMemory.getSequenceMemory(seq0);
+ }
+
+ @Test
+ public void testActivateAfterFirstCircuitStep() {
+ // activate LogicCircuit1 and check filters are adapters are created and made active
+ DynamicFilter filter0 = nodeMemory.getFilters()[0]; // B
+ DynamicFilter filter1 = nodeMemory.getFilters()[1]; // C
+ // [2] D is null
+
+ // DynamicFilters are created on demand from Protos
+ assertThat(nodeMemory.getFilters()).usingRecursiveComparison().isEqualTo(new DynamicFilter[] {filter0, filter1, null });
+
+ // Check correct activation of DynamicFilters
+ assertThat(nodeMemory.getActiveFilters()[0].size()).isEqualTo(1); // B is active
+ assertThat(nodeMemory.getActiveFilters()[0].getFirst()).isSameAs(filter0);
+ assertThat(nodeMemory.getActiveFilters()[1].size()).isEqualTo(1); // C is active
+ assertThat(nodeMemory.getActiveFilters()[1].getFirst()).isSameAs(filter1);
+ assertThat(nodeMemory.getActiveFilters()[2]).isNull(); // D is not yet active
+
+ // Check correct activation of SignalAdapters
+ SignalAdapter signal0 = sequenceMemory.getSignalAdapters()[0];
+ SignalAdapter signal1 = sequenceMemory.getSignalAdapters()[1];
+ assertThat(sequenceMemory.getSignalAdapters()).usingRecursiveComparison().isEqualTo(new SignalAdapter[] {signal0, signal1, null, null });
+ assertThat(sequenceMemory.getActiveSignalAdapters()).usingRecursiveComparison().isEqualTo(new SignalAdapter[] {signal0, signal1, null, null });
+ }
+
+ @Test
+ public void testDeactivateAndActivateAfterSecondCircuitStep() {
+ DynamicFilter filter0 = nodeMemory.getFilters()[0]; // B
+ DynamicFilter filter1 = nodeMemory.getFilters()[1]; // C
+
+ SignalAdapter signal0 = sequenceMemory.getSignalAdapters()[0];
+ SignalAdapter signal1 = sequenceMemory.getSignalAdapters()[1];
+
+ session.insert(new BEvent(0, "b"));
+ assertThat(getCurrentStep(sequencerMemory)).isEqualTo(0); // still step 0
+
+ session.insert(new CEvent(0, "c"));
+ // Next step, check everything was for LogicCircuit was de-activated and LogicCircuit1 was activated;
+ assertThat(getCurrentStep(sequencerMemory)).isEqualTo(1); // Now step 1
+
+ DynamicFilter filter2 = nodeMemory.getFilters()[2]; // D was created from its Proto
+
+ assertThat(nodeMemory.getFilters()).usingRecursiveComparison().isEqualTo(new DynamicFilter[] {filter0, filter1, filter2 }); // all three now created
+
+ // Check correct activation of DynamicFilters
+ assertThat(nodeMemory.getActiveFilters()[0].size()).isEqualTo(0); // B is deactived and now empty
+ assertThat(nodeMemory.getActiveFilters()[1].size()).isEqualTo(1); // C remains active
+ assertThat(nodeMemory.getActiveFilters()[1].getFirst()).isSameAs(filter1);
+ assertThat(nodeMemory.getActiveFilters()[2].size()).isEqualTo(1); // D is now active
+ assertThat(nodeMemory.getActiveFilters()[2].getFirst()).isSameAs(filter2);
+
+ // Check correct activation of SignalAdapters
+ SignalAdapter signal2 = sequenceMemory.getSignalAdapters()[2];
+ SignalAdapter signal3 = sequenceMemory.getSignalAdapters()[3];
+
+ assertThat(sequenceMemory.getSignalAdapters()).usingRecursiveComparison().isEqualTo(new SignalAdapter[] {signal0, signal1, signal2, signal3 });
+ assertThat(sequenceMemory.getActiveSignalAdapters()).usingRecursiveComparison().isEqualTo(new SignalAdapter[] {null, null, signal2, signal3 }); // 0 and 1 are no longer active
+ }
+
+ @Test
+ public void testDeactivateAfterEndStep() {
+ assertThat(getCurrentStep(sequencerMemory)).isEqualTo(0); // step 0
+ session.insert(new BEvent(0, "b"));
+ session.insert(new CEvent(0, "c"));
+ assertThat(getCurrentStep(sequencerMemory)).isEqualTo(1); // step 1
+ session.insert(new CEvent(0, "c"));
+ session.insert(new DEvent(0, "d"));
+
+ assertThat(getCurrentStep(sequencerMemory)).isEqualTo(-1); // Now step -1, which means it's finished.
+
+ // make sure there are no active SignalAapters or active Filters.
+ assertThat(sequenceMemory.getActiveSignalAdapters()).usingRecursiveComparison().isEqualTo(new SignalAdapter[] {null, null, null, null }); // 0 and 1 are no longer active
+ assertThat(nodeMemory.getActiveFilters()[0].size()).isEqualTo(0);
+ assertThat(nodeMemory.getActiveFilters()[1].size()).isEqualTo(0);
+ assertThat(nodeMemory.getActiveFilters()[2].size()).isEqualTo(0);
+ }
+
+ @Test
+ public void testUpdateOnDrivingTupleRestartsSequence() {
+ // Advance to step 1: insert B then C to satisfy gate 1
+ session.insert(new BEvent(0, "b"));
+ session.insert(new CEvent(0, "c"));
+ assertThat(getCurrentStep(sequencerMemory)).isEqualTo(1); // sequence is at step 1
+
+ // Update A — should stop the running sequence and restart it at step 0.
+ // fireAllRules() is required to flush the Phreak left-tuple update queue.
+ A a = (A) fhA0.getObject();
+ session.update(fhA0, a);
+ session.fireAllRules();
+
+ assertThat(getCurrentStep(sequencerMemory)).isEqualTo(0);
+ }
+}
diff --git a/drools-test-coverage/test-compiler-integration/src/test/java/org/drools/mvel/integrationtests/phreak/sequencing/PhreakSequencerLogicGateTest.java b/drools-test-coverage/test-compiler-integration/src/test/java/org/drools/mvel/integrationtests/phreak/sequencing/PhreakSequencerLogicGateTest.java
new file mode 100644
index 00000000000..5160798b577
--- /dev/null
+++ b/drools-test-coverage/test-compiler-integration/src/test/java/org/drools/mvel/integrationtests/phreak/sequencing/PhreakSequencerLogicGateTest.java
@@ -0,0 +1,156 @@
+/**
+ * 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.phreak.sequencing;
+
+import org.drools.base.rule.Pattern;
+import org.drools.core.common.InternalFactHandle;
+import org.drools.base.reteoo.sequencing.signalprocessors.LogicCircuit;
+import org.drools.base.reteoo.sequencing.signalprocessors.Gates;
+import org.drools.base.reteoo.sequencing.signalprocessors.LogicGate;
+import org.drools.base.reteoo.sequencing.signalprocessors.LogicGateOutputSignalProcessor;
+import org.drools.base.reteoo.sequencing.Sequence;
+import org.drools.base.reteoo.sequencing.signalprocessors.SignalIndex;
+import org.drools.base.reteoo.sequencing.steps.Step;
+import org.drools.base.reteoo.sequencing.signalprocessors.TerminatingSignalProcessor;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class PhreakSequencerLogicGateTest extends AbstractPhreakSequencerSubsequenceTest {
+
+ @BeforeEach
+ public void setup() {
+ initKBaseWithEmptyRule();
+ }
+
+ @Test
+ public void testAnd() {
+ LogicGate gate1 = new LogicGate((inputMask, sourceMask) -> Gates.and(inputMask, sourceMask),0,
+ new int[] {0, 1}, // B and C
+ new int[] {0, 1}, // Each SignalAdapter must be in a unique index for the Sequence
+ 0);
+
+ gate1.setOutput(TerminatingSignalProcessor.get());
+
+ LogicCircuit circuit1 = new LogicCircuit(gate1);
+
+ Sequence seq = new Sequence(0, Step.of(circuit1));
+ seq.setFilters(new Pattern[]{bpattern, cpattern, dpattern, epattern});
+ rule.addSequence(seq);
+ kbase.addPackage(pkg);
+
+ createSession();
+ assertThat(getCurrentStep(sequencerMemory)).isEqualTo(0); // step 0
+ session.insert(new BEvent(0, "b"));
+ assertThat(getCurrentStep(sequencerMemory)).isEqualTo(0); // step 0
+ session.insert(new CEvent(0, "c"));
+ assertThat(getCurrentStep(sequencerMemory)).isEqualTo(-1); // terminated
+
+ // reverse B and C
+ createSession();
+ assertThat(getCurrentStep(sequencerMemory)).isEqualTo(0); // step 0
+ session.insert(new CEvent(0, "c"));
+
+ assertThat(getCurrentStep(sequencerMemory)).isEqualTo(0); // step 0
+ session.insert(new BEvent(0, "b"));
+ assertThat(getCurrentStep(sequencerMemory)).isEqualTo(-1); // terminated
+ }
+
+ @Test
+ public void testOr() {
+ LogicGate gate1 = new LogicGate((inputMask, sourceMask) -> Gates.or(inputMask, sourceMask),0,
+ new int[] {0, 1}, // B and C
+ new int[] {0, 1}, // Each SignalAdapter must be in a unique index for the Sequence
+ 0);
+
+ gate1.setOutput(TerminatingSignalProcessor.get());
+
+ LogicCircuit circuit1 = new LogicCircuit(gate1);
+
+ Sequence seq = new Sequence(0, Step.of(circuit1));
+ seq.setFilters(new Pattern[]{bpattern, cpattern, dpattern, epattern});
+ rule.addSequence(seq);
+ kbase.addPackage(pkg);
+
+ createSession();
+ assertThat(getCurrentStep(sequencerMemory)).isEqualTo(0); // step 0
+ session.insert(new BEvent(0, "b"));
+
+ assertThat(getCurrentStep(sequencerMemory)).isEqualTo(-1); // terminated
+
+ // reverse B and C
+ createSession();
+ assertThat(getCurrentStep(sequencerMemory)).isEqualTo(0); // step 0
+ InternalFactHandle fhC0 = (InternalFactHandle) session.insert(new CEvent(0, "c"));
+
+ assertThat(getCurrentStep(sequencerMemory)).isEqualTo(-1); // terminated
+ }
+
+
+ @Test
+ public void testComposite() {
+ LogicGate gate1 = new LogicGate((inputMask, sourceMask) -> Gates.and(inputMask, sourceMask),0,
+ new int[] {0, 1}, // B and C
+ new int[] {0, 1}, // Each SignalAdapter must be in a unique index for the Sequence
+ 0);
+
+ LogicGate gate2 = new LogicGate((inputMask, sourceMask) -> Gates.and(inputMask, sourceMask),1,
+ new int[] {2}, // D
+ new int[] {2}, // D
+ 1);
+
+ gate1.setOutput(new LogicGateOutputSignalProcessor(SignalIndex.of(gate2, 2))); // gate bits must come after signals
+ gate2.setOutput(TerminatingSignalProcessor.get());
+ gate2.setInputGates(gate1);
+
+ LogicCircuit circuit1 = new LogicCircuit(gate1, gate2);
+
+ Sequence seq = new Sequence(0, Step.of(circuit1));
+ seq.setFilters(new Pattern[]{bpattern, cpattern, dpattern, epattern});
+ rule.addSequence(seq);
+ kbase.addPackage(pkg);
+
+ // D last
+ createSession();
+ assertThat(getCurrentStep(sequencerMemory)).isEqualTo(0); // step 0
+ session.insert(new BEvent(0, "b"));
+ assertThat(getCurrentStep(sequencerMemory)).isEqualTo(0); // step 0
+ session.insert(new CEvent(0, "c"));
+ assertThat(getCurrentStep(sequencerMemory)).isEqualTo(0); // step 0
+
+ // now it'll transition
+ session.insert(new DEvent(0, "d"));
+ assertThat(getCurrentStep(sequencerMemory)).isEqualTo(-1); // terminated
+
+ // change order, D first
+ createSession();
+ session.insert(new DEvent(0, "d"));
+ assertThat(getCurrentStep(sequencerMemory)).isEqualTo(0); // step 0
+
+ assertThat(getCurrentStep(sequencerMemory)).isEqualTo(0); // step 0
+ session.insert(new BEvent(0, "b"));
+ assertThat(getCurrentStep(sequencerMemory)).isEqualTo(0); // step 0
+
+ // now it'll transition
+ session.insert(new CEvent(0, "c"));
+ assertThat(getCurrentStep(sequencerMemory)).isEqualTo(-1); // terminated
+ }
+
+}
diff --git a/drools-model/drools-canonical-model/src/main/java/org/drools/model/view/ExistentialExprViewItem.java b/drools-test-coverage/test-compiler-integration/src/test/java/org/drools/mvel/integrationtests/phreak/sequencing/Predicate1.java
similarity index 53%
copy from drools-model/drools-canonical-model/src/main/java/org/drools/model/view/ExistentialExprViewItem.java
copy to drools-test-coverage/test-compiler-integration/src/test/java/org/drools/mvel/integrationtests/phreak/sequencing/Predicate1.java
index d74396d1070..07ea3962eda 100644
--- a/drools-model/drools-canonical-model/src/main/java/org/drools/model/view/ExistentialExprViewItem.java
+++ b/drools-test-coverage/test-compiler-integration/src/test/java/org/drools/mvel/integrationtests/phreak/sequencing/Predicate1.java
@@ -16,33 +16,8 @@
* specific language governing permissions and limitations
* under the License.
*/
-package org.drools.model.view;
+package org.drools.mvel.integrationtests.phreak.sequencing;
-import org.drools.model.Condition;
-import org.drools.model.Variable;
-
-public class ExistentialExprViewItem<T> extends AbstractExprViewItem<T> {
-
- private final Condition.Type type;
- private final ViewItem expression;
-
- public ExistentialExprViewItem(Condition.Type type, ViewItem expression) {
- super(expression.getFirstVariable());
- this.type = type;
- this.expression = expression;
- }
-
- public ViewItem getExpression() {
- return expression;
- }
-
- @Override
- public Variable<?>[] getVariables() {
- return expression.getVariables();
- }
-
- @Override
- public Condition.Type getType() {
- return type;
- }
+interface Predicate1<A> {
+ boolean test(A a);
}
diff --git a/drools-test-coverage/test-compiler-integration/src/test/java/org/drools/mvel/integrationtests/phreak/sequencing/SequenceCloneTest.java b/drools-test-coverage/test-compiler-integration/src/test/java/org/drools/mvel/integrationtests/phreak/sequencing/SequenceCloneTest.java
new file mode 100644
index 00000000000..4ef4c697cc1
--- /dev/null
+++ b/drools-test-coverage/test-compiler-integration/src/test/java/org/drools/mvel/integrationtests/phreak/sequencing/SequenceCloneTest.java
@@ -0,0 +1,65 @@
+/*
+ * 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.phreak.sequencing;
+
+import org.drools.base.reteoo.sequencing.Sequence;
+import org.drools.base.reteoo.sequencing.signalprocessors.Gates;
+import org.drools.base.reteoo.sequencing.signalprocessors.LogicCircuit;
+import org.drools.base.reteoo.sequencing.signalprocessors.LogicGate;
+import org.drools.base.reteoo.sequencing.signalprocessors.TerminatingSignalProcessor;
+import org.drools.base.reteoo.sequencing.steps.Step;
+import org.drools.base.rule.RuleConditionElement;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class SequenceCloneTest {
+
+ @Test
+ public void cloneDoesNotThrow() {
+ LogicGate gate = new LogicGate(
+ (a, b) -> Gates.and(a, b), 0,
+ new int[]{0}, new int[]{0}, 0);
+ gate.setOutput(TerminatingSignalProcessor.get());
+
+ Sequence original = new Sequence(0, Step.of(new LogicCircuit(gate)));
+
+ RuleConditionElement clone = original.clone();
+
+ assertThat(clone).isNotNull();
+ assertThat(clone).isNotSameAs(original);
+ }
+
+ @Test
+ public void clonePreservesDescriptors() {
+ LogicGate gate = new LogicGate(
+ (a, b) -> Gates.and(a, b), 0,
+ new int[]{0}, new int[]{0}, 0);
+ gate.setOutput(TerminatingSignalProcessor.get());
+
+ Sequence original = new Sequence(0, Step.of(new LogicCircuit(gate)));
+
+ Sequence clone = (Sequence) original.clone();
+
+ assertThat(clone.getSteps()).isSameAs(original.getSteps());
+ assertThat(clone.getGates()).isSameAs(original.getGates());
+ assertThat(clone.getFilters()).isSameAs(original.getFilters());
+ assertThat(clone).isNotSameAs(original);
+ }
+}
diff --git a/drools-test-coverage/test-compiler-integration/src/test/java/org/drools/mvel/integrationtests/phreak/sequencing/TestMemories.java b/drools-test-coverage/test-compiler-integration/src/test/java/org/drools/mvel/integrationtests/phreak/sequencing/TestMemories.java
new file mode 100644
index 00000000000..3ea3204e9a4
--- /dev/null
+++ b/drools-test-coverage/test-compiler-integration/src/test/java/org/drools/mvel/integrationtests/phreak/sequencing/TestMemories.java
@@ -0,0 +1,109 @@
+/*
+ * 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.phreak.sequencing;
+
+import org.drools.base.base.ClassObjectType;
+import org.drools.base.reteoo.DynamicFilter;
+import org.drools.base.reteoo.SignalAdapter;
+import org.drools.base.reteoo.sequencing.Sequence;
+import org.drools.base.reteoo.sequencing.Sequence.SequenceMemory;
+import org.drools.base.reteoo.sequencing.SequencerMemory;
+import org.drools.base.reteoo.sequencing.signalprocessors.Gates;
+import org.drools.base.reteoo.sequencing.signalprocessors.LogicCircuit;
+import org.drools.base.reteoo.sequencing.signalprocessors.LogicGate;
+import org.drools.base.reteoo.sequencing.signalprocessors.TerminatingSignalProcessor;
+import org.drools.base.reteoo.sequencing.steps.Step;
+import org.drools.base.rule.EntryPointId;
+import org.drools.base.rule.Pattern;
+import org.drools.base.util.LinkedList;
+import org.drools.core.ClockType;
+import org.drools.core.SessionConfiguration;
+import org.drools.core.common.InternalFactHandle;
+import org.drools.core.reteoo.LeftTuple;
+import org.drools.core.reteoo.MockLeftTupleSink;
+import org.drools.core.reteoo.ObjectTypeNode;
+import org.drools.core.reteoo.SequenceNode;
+import org.drools.core.reteoo.builder.BuildContext;
+import org.drools.kiesession.session.StatefulKnowledgeSessionImpl;
+import org.drools.mvel.integrationtests.phreak.A;
+import org.junit.jupiter.api.Test;
+import org.kie.api.runtime.conf.ThreadSafeOption;
+
+import java.util.Collections;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class TestMemories extends AbstractPhreakSequencerSubsequenceTest {
+
+ @Test
+ public void testInitialisedMemoryBeforeStart() {
+ initKBaseWithEmptyRule();
+ buildContext = new BuildContext(kbase, Collections.emptyList() );
+ buildContext.setRule(rule);
+
+ LogicGate gate1 = new LogicGate((inputMask, sourceMask) -> Gates.and(inputMask, sourceMask), 0,
+ new int[] {0, 1}, // B and C
+ new int[] {0, 1}, // Each SignalAdapter must be in a unique index for the Sequence
+ 0);
+ gate1.setOutput(TerminatingSignalProcessor.get());
+
+ LogicCircuit circuit1 = new LogicCircuit(gate1);
+
+ LogicGate gate2 = new LogicGate((inputMask, sourceMask) -> Gates.and(inputMask, sourceMask),1,
+ new int[] {1, 2}, // C and D, C Filter is re-used
+ new int[] {2, 3}, // Each SignalAdapter must be in a unique index for the Sequence
+ 0);
+ gate2.setOutput(TerminatingSignalProcessor.get());
+
+ LogicCircuit circuit2 = new LogicCircuit(gate2);
+
+ seq0 = new Sequence(0, Step.of(circuit1), Step.of(circuit2));
+ seq0.setFilters(new Pattern[]{bpattern, cpattern, dpattern});
+ rule.addSequence(seq0);
+ kbase.addPackage(pkg);
+
+ ObjectTypeNode aNode = kbase.getRete().getEntryPointNode(EntryPointId.DEFAULT).getObjectTypeNodes().get(new ClassObjectType(A.class));
+ snode = (SequenceNode) aNode.getSinks()[0].getSinks()[0];
+
+ SessionConfiguration sessionConf = kbase.getSessionConfiguration();
+ sessionConf.setOption(ThreadSafeOption.NO);
+ sessionConf.setClockType(ClockType.PSEUDO_CLOCK);
+
+ session = (StatefulKnowledgeSessionImpl) kbase.newKieSession(sessionConf, null);
+
+ InternalFactHandle fhA0 = (InternalFactHandle) session.insert(new A(0));
+ nodeMemory = session.getNodeMemory(snode);
+ LeftTuple lt = new LeftTuple(fhA0, snode, true);
+ lt.setContextObject(snode.createSequencerMemory(lt, new MockLeftTupleSink(buildContext.getNextNodeId(), buildContext), nodeMemory));
+ nodeMemory.getLeftTupleMemory().add(lt);
+ sequencerMemory = (SequencerMemory) lt.getContextObject();
+
+ SequenceMemory sequenceMemory = sequencerMemory.getOrCreateSequenceMemory(seq0, sequencerMemory.getData());
+ sequencerMemory.setChildSequenceMemory(sequenceMemory);
+
+ // make sure these are empty
+ assertThat(nodeMemory.getFilters()).usingRecursiveComparison().isEqualTo(new DynamicFilter[3]);
+ assertThat(nodeMemory.getActiveFilters()).usingRecursiveComparison().isEqualTo(new LinkedList[3]);
+ assertThat(nodeMemory.getActiveFilters()).usingRecursiveComparison().isEqualTo(new LinkedList[3]);
+ assertThat(sequenceMemory.getActiveSignalAdapters()).usingRecursiveComparison().isEqualTo(new SignalAdapter[4]);
+ assertThat(sequenceMemory.getSignalAdapters()).usingRecursiveComparison().isEqualTo(new SignalAdapter[4]);
+ assertThat(getCurrentStep(sequencerMemory)).isEqualTo(0);
+ }
+
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@kie.apache.org
For additional commands, e-mail: commits-help@kie.apache.org