drccrd opened a new issue, #7121:
URL: https://github.com/apache/incubator-kie/issues/7121

   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 
apache/incubator-kie-issues#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]

Reply via email to