javamultithreadingdroolsdrools-decisiontables

StatelessKieSession doesn't appear thread safe


I'm trying to optimize a section of the app by switching the single processing of a collection into running parallel under a lambda function.

I am building my KieContainer once as follows:

/**
 * From a decision table, create a rule base.
 * @param is the input stream to read
 * @return a rule container
 */
public KieContainer getKieContainerFromStream(InputStream is) throws IOException {
    SpreadsheetCompiler spreadsheetCompiler = new SpreadsheetCompiler();
    String rules = spreadsheetCompiler.compile(is, InputType.CSV);
    log.info("rules:" + rules);
    File rulesFile = FileUtil.stringToFile(rules, "rule", ".drl");

    KieFileSystem kieFileSystem = kieServices.newKieFileSystem();
    Resource resource = ResourceFactory.newFileResource(rulesFile);
    kieFileSystem.write(resource);
    KieBuilder kieBuilder = kieServices.newKieBuilder(kieFileSystem);
    kieBuilder.buildAll();
    KieModule kieModule = kieBuilder.getKieModule();
    KieContainer result = kieServices.newKieContainer(kieModule.getReleaseId());

    return result;
}

As you can see, I'm using a CSV decision table.

Now I inject that container into my object and run the following code inside my lambda.

protected void checkTheRules(PostponementRequest request) {
    StatelessKieSession kieSession = getPostponementRulesContainer().newStatelessKieSession();

    //apply the data
    PoolInfo pool = request.getPart().getActivePool();

    _log.debug("validating date for: courtLocation=" + pool.getCourtLocation() +
            ", daysBeforeSummons=" + pool.getDaysBeforeSummons() +
            ", requestDaysAfterSummons=" + request.getRequestDaysAfterSummons() +
            ", requestDaysAfterOrigSummons=" + request.getRequestDaysAfterOrigSummons() +
            ", requestedDayOfWeek=" + request.getRequestedDayOfWeek() +
            ", requestedDayOfMonth=" + request.getRequestedDayOfMonth()
    );

    List<String> list = new LinkedList<>();
    List<Object> parms = Arrays.asList(pool, request, list);
    kieSession.execute(parms);
}

Now let's say my loop contains 50 items that I'm looping through. That's 50 separate StatelessKieSession in 50 different threads. Every once in a while they will all work, but usually, I get the following one time:

jakarta.servlet.ServletException: Handler dispatch failed: java.lang.ExceptionInInitializerError
        at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1104) ~[spring-webmvc-6.2.7.jar:6.2.7]
        at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:979) ~[spring-webmvc-6.2.7.jar:6.2.7]
        at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1014) ~[spring-webmvc-6.2.7.jar:6.2.7]
        at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:903) ~[spring-webmvc-6.2.7.jar:6.2.7]
        at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:622) ~[servlet-api.jar:6.1]
        at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:885) ~[spring-webmvc-6.2.7.jar:6.2.7]
        at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:710) ~[servlet-api.jar:6.1]
        at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:130) ~[catalina.jar:11.0.8-dev]
        at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53) ~[tomcat-websocket.jar:11.0.8-dev]
        at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:109) ~[catalina.jar:11.0.8-dev]
        at org.apache.catalina.core.ApplicationDispatcher.invoke(ApplicationDispatcher.java:514) ~[catalina.jar:11.0.8-dev]
        at org.apache.catalina.core.ApplicationDispatcher.processRequest(ApplicationDispatcher.java:334) ~[catalina.jar:11.0.8-dev]
        at org.apache.catalina.core.ApplicationDispatcher.forward(ApplicationDispatcher.java:263) ~[catalina.jar:11.0.8-dev]
        at org.springframework.web.servlet.view.InternalResourceView.renderMergedOutputModel(InternalResourceView.java:171) ~[spring-webmvc-6.2.7.jar:6.2.7]
        at org.springframework.web.servlet.view.AbstractView.render(AbstractView.java:314) ~[spring-webmvc-6.2.7.jar:6.2.7]
        at org.springframework.web.servlet.DispatcherServlet.render(DispatcherServlet.java:1437) ~[spring-webmvc-6.2.7.jar:6.2.7]
        at org.springframework.web.servlet.DispatcherServlet.processDispatchResult(DispatcherServlet.java:1168) ~[spring-webmvc-6.2.7.jar:6.2.7]
        at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1106) ~[spring-webmvc-6.2.7.jar:6.2.7]
        at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:979) ~[spring-webmvc-6.2.7.jar:6.2.7]
        at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1014) ~[spring-webmvc-6.2.7.jar:6.2.7]
        at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:903) ~[spring-webmvc-6.2.7.jar:6.2.7]
        at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:622) ~[servlet-api.jar:6.1]
        at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:885) ~[spring-webmvc-6.2.7.jar:6.2.7]
        at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:710) ~[servlet-api.jar:6.1]
        at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:130) ~[catalina.jar:11.0.8-dev]
        at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53) ~[tomcat-websocket.jar:11.0.8-dev]
        at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:109) ~[catalina.jar:11.0.8-dev]
        at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:167) ~[catalina.jar:11.0.8-dev]
        at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:79) ~[catalina.jar:11.0.8-dev]
        at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:483) ~[catalina.jar:11.0.8-dev]
        at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:116) ~[catalina.jar:11.0.8-dev]
        at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:93) ~[catalina.jar:11.0.8-dev]
        at org.apache.catalina.valves.AbstractAccessLogValve.invoke(AbstractAccessLogValve.java:666) ~[catalina.jar:11.0.8-dev]
        at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74) ~[catalina.jar:11.0.8-dev]
        at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:343) ~[catalina.jar:11.0.8-dev]
        at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:396) ~[tomcat-coyote.jar:11.0.8-dev]
        at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:63) ~[tomcat-coyote.jar:11.0.8-dev]
        at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:903) ~[tomcat-coyote.jar:11.0.8-dev]
        at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1773) ~[tomcat-coyote.jar:11.0.8-dev]
        at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:52) ~[tomcat-coyote.jar:11.0.8-dev]
        at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) ~[tomcat-util.jar:11.0.8-dev]
        at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:637) ~[tomcat-util.jar:11.0.8-dev]
        at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:59) ~[tomcat-util.jar:11.0.8-dev]
        at java.base/java.lang.Thread.run(Thread.java:1583) [?:?]
Caused by: java.lang.ExceptionInInitializerError
        at java.base/jdk.internal.reflect.DirectConstructorHandleAccessor.newInstance(DirectConstructorHandleAccessor.java:62) ~[?:?]
        at java.base/java.lang.reflect.Constructor.newInstanceWithCaller(Constructor.java:502) ~[?:?]
        at java.base/java.lang.reflect.Constructor.newInstance(Constructor.java:486) ~[?:?]
        at java.base/java.util.concurrent.ForkJoinTask.getThrowableException(ForkJoinTask.java:540) ~[?:?]
        at java.base/java.util.concurrent.ForkJoinTask.reportException(ForkJoinTask.java:567) ~[?:?]
        at java.base/java.util.concurrent.ForkJoinTask.invoke(ForkJoinTask.java:670) ~[?:?]
        at java.base/java.util.stream.ForEachOps$ForEachOp.evaluateParallel(ForEachOps.java:160) ~[?:?]
        at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.evaluateParallel(ForEachOps.java:174) ~[?:?]
        at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:233) ~[?:?]
        at java.base/java.util.stream.ReferencePipeline.forEach(ReferencePipeline.java:596) ~[?:?]
        at java.base/java.util.stream.ReferencePipeline$Head.forEach(ReferencePipeline.java:765) ~[?:?]
        at com.gs.juror.bizstrategy.PostponementStrategy.testDates(PostponementStrategy.java:219) ~[juror-common-2.17.4.jar:2.17.4]
        at com.gs.juror.bizstrategy.PostponementStrategy.getValidDates(PostponementStrategy.java:208) ~[juror-common-2.17.4.jar:2.17.4]
        at com.gs.jxx.util.CalendarSupport.makeCSB(CalendarSupport.java:43) ~[juror-common-2.17.4.jar:2.17.4]
        at com.gs.jxx.web.controllers.PostponeController.setupView(PostponeController.java:553) ~[eJurorWeb-2.17.4.jar:2.17.4]
        at com.gs.jxx.web.controllers.PostponeController.setupForm(PostponeController.java:417) ~[eJurorWeb-2.17.4.jar:2.17.4]
        at com.gs.jxx.web.controllers.PostponeController.showForm(PostponeController.java:392) ~[eJurorWeb-2.17.4.jar:2.17.4]
        at com.gs.jxx.web.controllers.PostponeController.handleRequestInternal(PostponeController.java:103) ~[eJurorWeb-2.17.4.jar:2.17.4]
        at org.springframework.web.servlet.mvc.AbstractController.handleRequest(AbstractController.java:178) ~[spring-webmvc-6.2.7.jar:6.2.7]
        at org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter.handle(SimpleControllerHandlerAdapter.java:51) ~[spring-webmvc-6.2.7.jar:6.2.7]
        at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1089) ~[spring-webmvc-6.2.7.jar:6.2.7]
        ... 43 more
Caused by: java.lang.ExceptionInInitializerError
        at org.drools.mvel.MVELConstraint.jitEvaluator(MVELConstraint.java:329) ~[drools-mvel-10.0.0.jar:10.0.0]
        at org.drools.mvel.MVELConstraint.evaluate(MVELConstraint.java:293) ~[drools-mvel-10.0.0.jar:10.0.0]
        at org.drools.mvel.MVELConstraint.isAllowed(MVELConstraint.java:247) ~[drools-mvel-10.0.0.jar:10.0.0]
        at org.drools.core.reteoo.AlphaNode.assertObject(AlphaNode.java:124) ~[drools-core-10.0.0.jar:10.0.0]
        at org.drools.core.reteoo.CompositeObjectSinkAdapter.doPropagateAssertObject(CompositeObjectSinkAdapter.java:747) ~[drools-core-10.0.0.jar:10.0.0]
        at org.drools.core.reteoo.CompositeObjectSinkAdapter.propagateAssertObject(CompositeObjectSinkAdapter.java:580) ~[drools-core-10.0.0.jar:10.0.0]
        at org.drools.core.reteoo.CompositeObjectSinkAdapter.propagateAssertObject(CompositeObjectSinkAdapter.java:558) ~[drools-core-10.0.0.jar:10.0.0]
        at org.drools.core.reteoo.ObjectTypeNode.propagateAssert(ObjectTypeNode.java:217) ~[drools-core-10.0.0.jar:10.0.0]
        at org.drools.core.phreak.PropagationEntry$Insert.propagate(PropagationEntry.java:237) ~[drools-core-10.0.0.jar:10.0.0]
        at org.drools.core.phreak.PropagationEntry$Insert.internalExecute(PropagationEntry.java:250) ~[drools-core-10.0.0.jar:10.0.0]
        at org.drools.core.phreak.PropagationEntry.execute(PropagationEntry.java:56) ~[drools-core-10.0.0.jar:10.0.0]
        at org.drools.core.phreak.SynchronizedPropagationList.flush(SynchronizedPropagationList.java:105) ~[drools-core-10.0.0.jar:10.0.0]
        at org.drools.core.phreak.SynchronizedPropagationList.flush(SynchronizedPropagationList.java:100) ~[drools-core-10.0.0.jar:10.0.0]
        at org.drools.kiesession.agenda.DefaultAgenda.fireLoop(DefaultAgenda.java:610) ~[drools-kiesession-10.0.0.jar:10.0.0]
        at org.drools.kiesession.agenda.DefaultAgenda.internalFireAllRules(DefaultAgenda.java:573) ~[drools-kiesession-10.0.0.jar:10.0.0]
        at org.drools.kiesession.agenda.DefaultAgenda.fireAllRules(DefaultAgenda.java:565) ~[drools-kiesession-10.0.0.jar:10.0.0]
        at org.drools.kiesession.session.StatefulKnowledgeSessionImpl.internalFireAllRules(StatefulKnowledgeSessionImpl.java:1093) ~[drools-kiesession-10.0.0.jar:10.0.0]
        at org.drools.kiesession.session.StatefulKnowledgeSessionImpl.fireAllRules(StatefulKnowledgeSessionImpl.java:1084) ~[drools-kiesession-10.0.0.jar:10.0.0]
        at org.drools.kiesession.session.StatefulKnowledgeSessionImpl.fireAllRules(StatefulKnowledgeSessionImpl.java:1068) ~[drools-kiesession-10.0.0.jar:10.0.0]
        at org.drools.kiesession.session.StatelessKnowledgeSessionImpl.execute(StatelessKnowledgeSessionImpl.java:284) ~[drools-kiesession-10.0.0.jar:10.0.0]
        at com.gs.jxx.bizstrategy.PostponementStrategy.checkTheRules(PostponementStrategy.java:822) ~[juror-common-2.17.4.jar:2.17.4]
        at com.gs.jxx.bizstrategy.PostponementStrategy.validateRequest(PostponementStrategy.java:793) ~[juror-common-2.17.4.jar:2.17.4]
        at com.gs.jxx.bizstrategy.PostponementStrategy.lambda$testDates$0(PostponementStrategy.java:225) ~[juror-common-2.17.4.jar:2.17.4]
        at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.accept(ForEachOps.java:184) ~[?:?]
        at java.base/java.util.TreeMap$KeySpliterator.forEachRemaining(TreeMap.java:3099) ~[?:?]
        at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:509) ~[?:?]
        at java.base/java.util.stream.ForEachOps$ForEachTask.compute(ForEachOps.java:291) ~[?:?]
        at java.base/java.util.concurrent.CountedCompleter.exec(CountedCompleter.java:754) ~[?:?]
        at java.base/java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:387) ~[?:?]
        at java.base/java.util.concurrent.ForkJoinPool$WorkQueue.topLevelExec(ForkJoinPool.java:1310) ~[?:?]
        at java.base/java.util.concurrent.ForkJoinPool.scan(ForkJoinPool.java:1841) ~[?:?]
        at java.base/java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1806) ~[?:?]
        at java.base/java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:188) ~[?:?]
Caused by: java.lang.NullPointerException: Cannot invoke "org.kie.api.concurrent.KieExecutors.getExecutor()" because the return value of "org.kie.internal.concurrent.ExecutorProviderFactory.getExecutorProvider()" is null
        at org.drools.mvel.MVELConstraint$ExecutorHolder.<clinit>(MVELConstraint.java:355) ~[drools-mvel-10.0.0.jar:10.0.0]
        at org.drools.mvel.MVELConstraint.jitEvaluator(MVELConstraint.java:329) ~[drools-mvel-10.0.0.jar:10.0.0]
        at org.drools.mvel.MVELConstraint.evaluate(MVELConstraint.java:293) ~[drools-mvel-10.0.0.jar:10.0.0]
        at org.drools.mvel.MVELConstraint.isAllowed(MVELConstraint.java:247) ~[drools-mvel-10.0.0.jar:10.0.0]
        at org.drools.core.reteoo.AlphaNode.assertObject(AlphaNode.java:124) ~[drools-core-10.0.0.jar:10.0.0]
        at org.drools.core.reteoo.CompositeObjectSinkAdapter.doPropagateAssertObject(CompositeObjectSinkAdapter.java:747) ~[drools-core-10.0.0.jar:10.0.0]
        at org.drools.core.reteoo.CompositeObjectSinkAdapter.propagateAssertObject(CompositeObjectSinkAdapter.java:580) ~[drools-core-10.0.0.jar:10.0.0]
        at org.drools.core.reteoo.CompositeObjectSinkAdapter.propagateAssertObject(CompositeObjectSinkAdapter.java:558) ~[drools-core-10.0.0.jar:10.0.0]
        at org.drools.core.reteoo.ObjectTypeNode.propagateAssert(ObjectTypeNode.java:217) ~[drools-core-10.0.0.jar:10.0.0]
        at org.drools.core.phreak.PropagationEntry$Insert.propagate(PropagationEntry.java:237) ~[drools-core-10.0.0.jar:10.0.0]
        at org.drools.core.phreak.PropagationEntry$Insert.internalExecute(PropagationEntry.java:250) ~[drools-core-10.0.0.jar:10.0.0]
        at org.drools.core.phreak.PropagationEntry.execute(PropagationEntry.java:56) ~[drools-core-10.0.0.jar:10.0.0]
        at org.drools.core.phreak.SynchronizedPropagationList.flush(SynchronizedPropagationList.java:105) ~[drools-core-10.0.0.jar:10.0.0]
        at org.drools.core.phreak.SynchronizedPropagationList.flush(SynchronizedPropagationList.java:100) ~[drools-core-10.0.0.jar:10.0.0]
        at org.drools.kiesession.agenda.DefaultAgenda.fireLoop(DefaultAgenda.java:610) ~[drools-kiesession-10.0.0.jar:10.0.0]
        at org.drools.kiesession.agenda.DefaultAgenda.internalFireAllRules(DefaultAgenda.java:573) ~[drools-kiesession-10.0.0.jar:10.0.0]
        at org.drools.kiesession.agenda.DefaultAgenda.fireAllRules(DefaultAgenda.java:565) ~[drools-kiesession-10.0.0.jar:10.0.0]
        at org.drools.kiesession.session.StatefulKnowledgeSessionImpl.internalFireAllRules(StatefulKnowledgeSessionImpl.java:1093) ~[drools-kiesession-10.0.0.jar:10.0.0]
        at org.drools.kiesession.session.StatefulKnowledgeSessionImpl.fireAllRules(StatefulKnowledgeSessionImpl.java:1084) ~[drools-kiesession-10.0.0.jar:10.0.0]
        at org.drools.kiesession.session.StatefulKnowledgeSessionImpl.fireAllRules(StatefulKnowledgeSessionImpl.java:1068) ~[drools-kiesession-10.0.0.jar:10.0.0]
        at org.drools.kiesession.session.StatelessKnowledgeSessionImpl.execute(StatelessKnowledgeSessionImpl.java:284) ~[drools-kiesession-10.0.0.jar:10.0.0]
        at com.gs.jxx.bizstrategy.PostponementStrategy.checkTheRules(PostponementStrategy.java:822) ~[juror-common-2.17.4.jar:2.17.4]
        at com.gs.jxx.bizstrategy.PostponementStrategy.validateRequest(PostponementStrategy.java:793) ~[juror-common-2.17.4.jar:2.17.4]
        at com.gs.jxx.bizstrategy.PostponementStrategy.lambda$testDates$0(PostponementStrategy.java:225) ~[juror-common-2.17.4.jar:2.17.4]
        at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.accept(ForEachOps.java:184) ~[?:?]
        at java.base/java.util.TreeMap$KeySpliterator.forEachRemaining(TreeMap.java:3099) ~[?:?]
        at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:509) ~[?:?]
        at java.base/java.util.stream.ForEachOps$ForEachTask.compute(ForEachOps.java:291) ~[?:?]
        at java.base/java.util.concurrent.CountedCompleter.exec(CountedCompleter.java:754) ~[?:?]
        at java.base/java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:387) ~[?:?]
        at java.base/java.util.concurrent.ForkJoinPool$WorkQueue.topLevelExec(ForkJoinPool.java:1310) ~[?:?]
        at java.base/java.util.concurrent.ForkJoinPool.scan(ForkJoinPool.java:1841) ~[?:?]
        at java.base/java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1806) ~[?:?]
        at java.base/java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:188) ~[?:?]

Now, according to my research, the kie session is thread safe and I could actually share amongst threads, but I'm not doing that and I'm still getting a thread error.

The fact that it works fine single threaded and sometimes works multi-threaded I think means the problem is in drools.


Solution

  • Different threads are calling parts of Drools' internal logic (most likely JIT compiler) at the same time, which causes the internal state to become corrupted.

    Even though KieContainer is thread-safe, Drools internals, especially those involving dynamic compilation (like JIT), are often not fully thread-safe during initial loading.

    at org.drools.mvel.MVELConstraint$ExecutorHolder.<clinit>(MVELConstraint.java:355)
    

    This line indicates a race condition during the static initialization of MVELConstraint.ExecutorHolder, which is a static nested class used to manage JIT evaluation.

    The class is not properly synchronized, and under concurrent access (from your parallel .forEach()), it can be initialized in an invalid state.

    That’s why it sometimes works: if one thread finishes initializing everything first

    And sometimes fails: if threads collide while it's still loading.

    Disable JIT in Drools 6.2 with Java 8

    One of the options is to disable JIT compiler in drools, as stated in this other question.

    In drools 10 you can (discovered by @Thom):

    System.setProperty("drools.jittingThreshold", "-1");