Rikkola opened a new issue, #7247:
URL: https://github.com/apache/incubator-kie/issues/7247
### Describe the bug
Something AI review picked up and needs to be investigated.
## Summary
`SegmentMemorySupportImpl.initializeChildSegmentsIfNeeded()` contains a
classic check-then-act
race condition. The guard:
if (!smem.isEmpty()) return;
…is followed by an unsynchronised loop that calls `smem.add(childSmem)`.
Neither
`LinkedList.isEmpty()` nor `LinkedList.add()` is protected by any lock, so
two threads that
both observe `isEmpty() == true` before either has appended a child will
both run the full
population loop — corrupting the child-segment linked list.
## Affected code
`drools-core/src/main/java/org/drools/core/phreak/SegmentMemorySupportImpl.java`,
lines 214–224
(previously `RuntimeSegmentUtilities.createChildSegments`, introduced in
DROOLS-7238 / #4812, November 2022)
## Precondition
The race window opens when ALL of the following hold:
1. `ThreadSafeOption.NO` is set on the session (disables the
`NamedEntryPoint` `ReentrantLock`).
2. The LIA for the inserted type is a **split node** (`sinkPropagator.size()
> 1`), which makes
`sm.getTipNode() == liaNode` true in
`LeftInputAdapterNode.doInsertObject`, routing into
`initializeChildSegmentsIfNeeded`.
3. Two threads insert into the same session simultaneously on a fresh
(un-initialised) segment.
In default thread-safe mode the `NamedEntryPoint` lock serialises `insert()`
calls and the race
cannot occur.
## Observable symptom
The corrupted `LinkedList` node pointers cause one or more rule firings to
be lost. In a
two-rule/two-fact scenario the expected count is 4; the race consistently
produces 3.
In severe races it can also produce `NoSuchElementException` from
`TreeSetQueue` during
`fireAllRules`.
## Failing test
`ConcurrentChildSegmentInitTest` ( See reproducer section )
— fails on attempt 0, both exec-model variants, with:
Expected size: 4 but was: 3 in: ["R1:fact0", "R2:fact0", "R2:fact1"]
## Fix direction
Synchronise the check-then-act in `initializeChildSegmentsIfNeeded` — either
with a
`synchronized` block on `smem`, or by replacing the two-step isEmpty/add
with a
compare-and-set. Alternatively, ensure callers always hold a segment-level
lock before
invoking this method.
### Expected behavior
_No response_
### Actual behavior
_No response_
### How to Reproduce?
```
/*
* 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.concurrency;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CompletionService;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.ExecutorCompletionService;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.stream.Stream;
import org.drools.base.base.ClassObjectType;
import org.drools.core.SessionConfiguration;
import org.drools.core.impl.InternalRuleBase;
import org.drools.core.reteoo.LeftInputAdapterNode;
import org.drools.core.reteoo.ObjectTypeNode;
import org.drools.testcoverage.common.util.KieBaseTestConfiguration;
import org.drools.testcoverage.common.util.KieBaseUtil;
import org.drools.testcoverage.common.util.TestParametersUtil2;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import org.kie.api.KieBase;
import org.kie.api.runtime.KieSession;
import org.kie.api.runtime.conf.ThreadSafeOption;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatCode;
/**
* Proves the check-then-act race in
SegmentMemorySupportImpl.initializeChildSegmentsIfNeeded.
*
* The guard is:
* if (!smem.isEmpty()) return;
* followed by an unsynchronised loop that calls smem.add(childSmem).
*
* When ThreadSafeOption.NO is set the NamedEntryPoint lock is null, so
insert() calls are not
* serialised. Two threads inserting simultaneously into a session whose LIA
is a split node
* (sinkPropagator.size() > 1) can both observe isEmpty() == true before
either has appended any
* child — both enter the loop and each appends all N sinks, leaving 2×N
child segments instead of N.
*
* Observable symptom: rules fire more times than expected.
* For 2 facts × 2 rules the expected count is 4; a duplicate segment causes
8 (or more).
*/
public class ConcurrentChildSegmentInitTest {
// Two rules sharing the same first pattern type force the LIA to be a
split node
// (sinkPropagator.size() == 2), which makes sm.getTipNode() == liaNode
in doInsertObject,
// opening the initializeChildSegmentsIfNeeded code path.
private static final String DRL =
"package org.drools.test;\n" +
"global java.util.List results;\n" +
"rule R1 when\n" +
" $s : String()\n" +
"then\n" +
" results.add(\"R1:\" + $s);\n" +
"end\n" +
"rule R2 when\n" +
" $s : String()\n" +
"then\n" +
" results.add(\"R2:\" + $s);\n" +
"end\n";
public static Stream<KieBaseTestConfiguration> parameters() {
return
TestParametersUtil2.getKieBaseCloudConfigurations(true).stream();
}
/**
* Two threads race to insert the very first fact into a session built
without locking
* (ThreadSafeOption.NO). Both reach initializeChildSegmentsIfNeeded
before either has
* added any child segment. The unguarded loop lets both threads append
all children,
* doubling the list from 2 to 4 entries and causing 8 rule firings
instead of 4.
*
* The test creates a fresh session per attempt and repeats many times
so that the race
* manifests reliably even on low-parallelism hardware.
*/
@ParameterizedTest(name = "KieBase type={0}")
@MethodSource("parameters")
public void
testConcurrentFirstInsertDoesNotDuplicateChildSegments(KieBaseTestConfiguration
kieBaseTestConfiguration)
throws Exception {
final KieBase kieBase =
KieBaseUtil.getKieBaseFromKieModuleFromDrl("test", kieBaseTestConfiguration,
DRL);
// Sanity-check the network topology: the LIA for String must be a
split node
// (2 sinks, one per rule) so that sm.getTipNode() == liaNode holds
in doInsertObject.
final ObjectTypeNode stringOtn = getObjectTypeNode(kieBase,
String.class);
assertThat(stringOtn).as("String OTN not found in rete").isNotNull();
final LeftInputAdapterNode liaNode =
(LeftInputAdapterNode)
stringOtn.getObjectSinkPropagator().getSinks()[0];
assertThat(liaNode.getSinkPropagator().size())
.as("LIA must be a split node (2 sinks) for the race window
to open")
.isEqualTo(2);
// Repeat many times: a race condition is non-deterministic but
becomes
// statistically certain over enough attempts on a fresh
(un-initialised) session.
for (int attempt = 0; attempt < 200; attempt++) {
// Disable session-level locking so that concurrent insert()
calls are not serialised.
// This is the condition under which
initializeChildSegmentsIfNeeded is exposed to the race.
final SessionConfiguration sessionConf =
(SessionConfiguration) ((InternalRuleBase)
kieBase).getSessionConfiguration();
sessionConf.setOption(ThreadSafeOption.NO);
final KieSession ksession = kieBase.newKieSession(sessionConf,
null);
final List<String> results = new ArrayList<>();
ksession.setGlobal("results", results);
// A barrier aligns both threads so they reach the isEmpty()
check simultaneously.
final CyclicBarrier startBarrier = new CyclicBarrier(2);
final ExecutorService executor = Executors.newFixedThreadPool(2,
r -> {
final Thread t = new Thread(r);
t.setDaemon(true);
return t;
});
try {
final CompletionService<Void> ecs = new
ExecutorCompletionService<>(executor);
for (int i = 0; i < 2; i++) {
final String fact = "fact" + i;
ecs.submit(() -> {
try {
startBarrier.await();
} catch (InterruptedException |
BrokenBarrierException e) {
Thread.currentThread().interrupt();
return null;
}
ksession.insert(fact);
return null;
});
}
for (int i = 0; i < 2; i++) {
ecs.take().get();
}
} finally {
executor.shutdownNow();
}
// fireAllRules itself may throw if the race corrupted a sorted
agenda structure
// (e.g. NoSuchElementException from TreeSetQueue) — that is
also a valid failure signal.
final KieSession sessionRef = ksession;
assertThatCode(sessionRef::fireAllRules)
.as("Attempt " + attempt + ": fireAllRules must not
throw. " +
"The initializeChildSegmentsIfNeeded race can
corrupt internal data structures " +
"such as the agenda's sorted queue.")
.doesNotThrowAnyException();
// 2 facts × 2 rules = exactly 4 firings.
// A duplicate child segment (caused by the race) produces ≠ 4
(extra or missing firings).
assertThat(results)
.as("Attempt " + attempt + ": expected exactly 4 rule
firings (2 facts × 2 rules). " +
"Duplicate or missing child segments from the
initializeChildSegmentsIfNeeded race " +
"cause incorrect firing counts. Got: " + results)
.hasSize(4);
ksession.dispose();
}
}
private static ObjectTypeNode getObjectTypeNode(KieBase kieBase,
Class<?> type) {
for (ObjectTypeNode otn : ((InternalRuleBase)
kieBase).getRete().getObjectTypeNodes()) {
if (otn.getObjectType() instanceof ClassObjectType &&
((ClassObjectType) otn.getObjectType()).getClassType()
== type) {
return otn;
}
}
return null;
}
}
```
### Output of `uname -a` or `ver`
_No response_
### Output of `java -version`
_No response_
### GraalVM version (if different from Java)
_No response_
### Kogito version or git rev (or at least Quarkus version if you are using
Kogito via Quarkus platform BOM)
_No response_
### Build tool (ie. output of `mvnw --version` or `gradlew --version`)
_No response_
### Additional information
_No response_
--
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]