drccrd opened a new issue, #2402:
URL: https://github.com/apache/incubator-kie-issues/issues/2402
A conditional element that two rules wrap in `not(...)` and in `exists(...)`
respectively — so one subnetwork (`TupleToObjectNode`) feeds two beta nodes —
intermittently throws a `NullPointerException` on the subnetwork right-staging
delete path.
Stack trace
```
java.lang.NullPointerException: Cannot invoke
"org.drools.core.reteoo.TupleImpl.setStagedNext(org.drools.core.reteoo.TupleImpl)"
because "tuple" is null
at
org.drools.core.common.TupleSetsImpl.removeInsert(TupleSetsImpl.java:188)
at
org.drools.core.phreak.RuleNetworkEvaluatorImpl.doSubnetwork2(RuleNetworkEvaluatorImpl.java:648)
at
org.drools.core.phreak.RuleNetworkEvaluatorImpl.evaluateEndNode(RuleNetworkEvaluatorImpl.java:357)
at
org.drools.core.phreak.RuleNetworkEvaluatorImpl.innerEval(RuleNetworkEvaluatorImpl.java:318)
at
org.drools.core.phreak.RuleNetworkEvaluatorImpl.doSubnetwork(RuleNetworkEvaluatorImpl.java:578)
at
org.drools.core.phreak.RuleNetworkEvaluatorImpl.evaluateBetaNode(RuleNetworkEvaluatorImpl.java:522)
at
org.drools.core.phreak.RuleNetworkEvaluatorImpl.evaluateNonTerminalNode(RuleNetworkEvaluatorImpl.java:389)
at
org.drools.core.phreak.RuleNetworkEvaluatorImpl.innerEval(RuleNetworkEvaluatorImpl.java:324)
at
org.drools.core.phreak.RuleNetworkEvaluatorImpl.outerEval(RuleNetworkEvaluatorImpl.java:256)
at
org.drools.core.phreak.RuleNetworkEvaluatorImpl.evaluateNetwork(RuleNetworkEvaluatorImpl.java:148)
at
org.drools.core.phreak.RuleExecutor.evaluateNetwork(RuleExecutor.java:225)
```
Root cause
`TupleSetsImpl.removeInsert` unlinks a tuple from the staged insert list,
but guards only one of the two link updates:
```java
} else {
TupleImpl next = getNextTuple(tuple);
TupleImpl previous = getPreviousTuple(tuple);
if ( next != null ) {
setPreviousTuple( next, previous ); // guarded
}
setNextTuple( previous, next ); // NOT guarded -> NPE when
previous == null
}
```
`setNextTuple(TupleImpl tuple, TupleImpl stagedNext)` dereferences its first
parameter, which is where the `because "tuple" is null` message comes from —
the receiver, not the argument.
It is reached from the subnetwork delete loop in
`RuleNetworkEvaluatorImpl.doSubnetwork2`:
```java
switch (subnetworkTuple.getStagedTypeOnRight()) {
// handle clash with already staged entries
case Tuple.INSERT:
rightTuples.removeInsert(subnetworkTuple.moveStagingFromLeftToRight());
```
Replacing the unguarded line with a diagnostic that logs the tuple state
instead of throwing (current `main`) yields:
```
DIAG-NPE removeInsert tuple=SubnetworkTuple stagedType=3
sink=TupleToObjectNode#30 insertFirst=set
```
So at the point of failure the tuple's **left** staged type is `DELETE` (3)
while `getStagedTypeOnRight()` reports `INSERT`, and the staged insert list is
non-empty (`insertFirst=set`) but this tuple is neither its head nor has a
`previous` link — it is not actually linked into the list that `removeInsert`
is unlinking it from. `SubnetworkTuple` keeps two independent staging link sets
(left and `...OnRight`), and `moveStagingFromLeftToRight()` copies the right
links into the left fields so the generic `TupleSetsImpl` unlink code can
operate on them; when those two states diverge, the unlink runs against the
wrong list.
`removeDelete` and `removeUpdate` contain the identical unguarded line.
Conditions
- One subnetwork CE structurally shared by two beta nodes: the same `X(...,
$list : items) and Y(...) from $list` written once under `not(...)` and once
under `exists(...)`. Sharing is what makes `doSubnetwork2` hand the first node
the `SubnetworkTuple` and every further node a peer of it.
- An insert and a delete of the same subnetwork tuple staged in one
`doSubnetwork2` batch — in our case the subnetwork source is `insertLogical`-ed
and its support withdrawn during the same evaluation cycle.
- More than one subnetwork tuple staged concurrently. With a single tuple it
*is* `insertFirst` and takes the safe branch, so the bug needs the deleted
tuple not to be the head.
- CLOUD mode, plain DRL. No STREAM, temporal constructs or sequencing
involved.
Affected versions
Reproduced on **10.2.0** and on current **main**. 10.2.x is a release branch
with no engine commits since it forked, so the source is the same on both.
Reproducer
I do not yet have a minimal standalone reproducer, and I would rather report
the analysis than sit on it. It reproduces intermittently (roughly one run in
four) in a large rule set — ~10 DRL files, a few hundred rules, multiple
timepoints — where the two rules below share the CE:
```drools
rule "A"
when
InputFact( $value : value )
not(
AnnotationsOutputFact( ..., $annotations : annotations )
and Annotation( answer == $value, kind == Kind.Invisible ) from
$annotations
)
then ... end
rule "B"
when
InputFact( $value : value )
exists(
AnnotationsOutputFact( ..., $annotations : annotations ) //
identical CE
and Annotation( answer == $value, kind == Kind.Invisible ) from
$annotations
)
OtherFact( ... )
then ... end
```
Adding one extra constraint to only the `exists(...)` copy, so the two CEs
are no longer structurally identical and the subnetwork is no longer shared,
makes it disappear completely. That is the workaround currently in use.
Attempts at a small reproducer that build the same topology (verified: one
`TupleToObjectNode` feeding both a `NotNode` and an `ExistsNode`) with logical
insertion, in-cycle support withdrawal and several concurrent subnetwork tuples
did not trigger it — the left/right staged-type divergence is the part I could
not force from a small rule set. Happy to test a patch against the large rule
set, which does reproduce.
Suggested fix
Guard `previous` the way `next` is already guarded, in `removeInsert`,
`removeDelete` and `removeUpdate` — the same shape of fix as #2338 /
apache/incubator-kie#6756, which guarded the sibling subnetwork-not
right-delete path. Whether the guard is sufficient or the divergence between
`stagedType` and `stagedTypeOnRight` should be prevented upstream of it is
worth a maintainer's judgement.
🤖 Analysis performed with [Claude Code](https://claude.com/claude-code)
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]