Search code examples
javamultithreadinggraalvm

Graalvm Polyglot Thread issue in Java Spring boot application


From Spring boot project we are calling GraalVM for processing some rules written in JavaScript. But when I am calling GraalVM using multiple threads, it is giving the below exception. If we use synchronized then the below issue is not coming. I know JavaScript runs on a single thread but I wanted to run graalVM using multiple threads. Is there any way to run multiple GraalVMs on multiple threads simultneously?

Some more details about project structure: I have kafka consumer, which is receiving huge messages from Kafka topics and then calling graalvm to process them using some JavaScript rules.

2020-08-14 11:00:28.363 [te-4-C-1] DEBUG c.e.d.j.t.RuleExecutor#110 Function MessageBroker_get_error_info executed in 192546300 ns. 2020-08-14 11:00:28.363 [te-0-C-1] ERROR c.e.d.j.t.RuleExecutor#102 Unexpected error executing TE rule: customer_entities function : com.oracle.truffle.polyglot.PolyglotIllegalStateException: Multi threaded access requested by thread Thread[te-0-C-1,5,main] but is not allowed for language(s) js. at com.oracle.truffle.polyglot.PolyglotContextImpl.throwDeniedThreadAccess(PolyglotContextImpl.java:649) at com.oracle.truffle.polyglot.PolyglotContextImpl.checkAllThreadAccesses(PolyglotContextImpl.java:567) at com.oracle.truffle.polyglot.PolyglotContextImpl.enterThreadChanged(PolyglotContextImpl.java:486) at com.oracle.truffle.polyglot.PolyglotContextImpl.enter(PolyglotContextImpl.java:447) at com.oracle.truffle.polyglot.HostToGuestRootNode.execute(HostToGuestRootNode.java:82) at com.oracle.truffle.api.impl.DefaultCallTarget.call(DefaultCallTarget.java:102) at com.oracle.truffle.api.impl.DefaultCallTarget$2.call(DefaultCallTarget.java:130) at com.oracle.truffle.polyglot.PolyglotValue$InteropValue.getMember(PolyglotValue.java:2259) at org.graalvm.polyglot.Value.getMember(Value.java:280) at com.ericsson.datamigration.js.transformation.RuleExecutor.run(RuleExecutor.java:73) at com.ericsson.datamigration.js.transformation.TransformationProcess.process(TransformationProcess.java:149) at com.ericsson.datamigration.bridging.converter.core.wfm.yaml.steps.ApplyTransformationMessageBroker.execute(ApplyTransformationMessageBroker.java:104) at com.ericsson.datamigration.bss.wfm.core.AbstractStep.run(AbstractStep.java:105) at com.ericsson.datamigration.bss.wfm.yaml.definition.SimpleWorkflow.execute(SimpleWorkflow.java:103) at com.ericsson.datamigration.bss.wfm.core.AbstractProcessor.run(AbstractProcessor.java:64) at com.ericsson.datamigration.bss.wfm.yaml.definition.ConditionalWorkflow.execute(ConditionalWorkflow.java:95) at com.ericsson.datamigration.bss.wfm.core.AbstractProcessor.run(AbstractProcessor.java:64) at com.ericsson.datamigration.bss.wfm.application.WorkflowManagerApplication.process(WorkflowManagerApplication.java:243) at com.ericsson.datamigration.bridging.dispatcher.core.kafka.consumer.KafkaMessageConsumer.processRequest(KafkaMessageConsumer.java:198) at com.ericsson.datamigration.bridging.dispatcher.core.kafka.consumer.KafkaMessageConsumer.listen(KafkaMessageConsumer.java:89) at sun.reflect.GeneratedMethodAccessor114.invoke(Unknown Source) at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source) at java.lang.reflect.Method.invoke(Unknown Source) at org.springframework.messaging.handler.invocation.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:181) at org.springframework.messaging.handler.invocation.InvocableHandlerMethod.invoke(InvocableHandlerMethod.java:114) at org.springframework.kafka.listener.adapter.HandlerAdapter.invoke(HandlerAdapter.java:48) at org.springframework.kafka.listener.adapter.MessagingMessageListenerAdapter.invokeHandler(MessagingMessageListenerAdapter.java:248) at org.springframework.kafka.listener.adapter.RecordMessagingMessageListenerAdapter.onMessage(RecordMessagingMessageListenerAdapter.java:80) at org.springframework.kafka.listener.adapter.RecordMessagingMessageListenerAdapter.onMessage(RecordMessagingMessageListenerAdapter.java:51) at org.springframework.kafka.listener.KafkaMessageListenerContainer$ListenerConsumer.doInvokeRecordListener(KafkaMessageListenerContainer.java:1071) at org.springframework.kafka.listener.KafkaMessageListenerContainer$ListenerConsumer.doInvokeWithRecords(KafkaMessageListenerContainer.java:1051) at org.springframework.kafka.listener.KafkaMessageListenerContainer$ListenerConsumer.invokeRecordListener(KafkaMessageListenerContainer.java:998) at org.springframework.kafka.listener.KafkaMessageListenerContainer$ListenerConsumer.invokeListener(KafkaMessageListenerContainer.java:866) at org.springframework.kafka.listener.KafkaMessageListenerContainer$ListenerConsumer.run(KafkaMessageListenerContainer.java:724) at java.util.concurrent.Executors$RunnableAdapter.call(Unknown Source) at java.util.concurrent.FutureTask.run(Unknown Source) at java.lang.Thread.run(Unknown Source)


Solution

  • Yes you can run multiple GraalVM contexts simultaneously.

    As described in the following article: https://medium.com/graalvm/multi-threaded-java-javascript-language-interoperability-in-graalvm-2f19c1f9c37b

    GraalVM’s JavaScript runtime supports parallel execution via multiple threads in a simple yet powerful way, which we believe is convenient for a variety of embedding scenarios. The model is based on the following three simple rules:

    • In a polyglot application, an arbitrary number of JS runtimes can be created, but they should be used by one thread at a time.
    • Concurrent access to Java objects is allowed: any Java object can be accessed by any Java or JavaScript thread, concurrently.
    • Concurrent access to JavaScript objects is not allowed: any JavaScript object cannot be accessed by more than one thread at a time.

    GraalVM enforces these rules at runtime, therefore making it easier and safer to reason about parallel and concurrent execution in a polyglot application.

    So when you're trying to access a JS object (function) concurrently from multiple threads, you see the exception you showed.

    What you can do is ensure that only 1 thread has access to your JS objects. One way to do this is to use synchronization. Another -- creating multiple Context objects 1 per thread.

    This approach is used in this demo application: https://github.com/graalvm/graalvm-demos/tree/master/js-java-async-helidon

    it uses a context provider helper class:

    private static class ContextProvider {
    
            private final Context context;
            private final ReentrantLock lock;
    
            ContextProvider(Context cx) {
                this.context = cx;
                this.lock = new ReentrantLock();
            }
    
            Context getContext() {
                return context;
            }
    
            Lock getLock() {
                return lock;
            }
        }
    

    And initializes them for every thread using a threadlocal:

        private final ThreadLocal<ContextProvider> jsContext = ThreadLocal.withInitial(() -> {
            /*
             * For simplicity, allow ALL accesses. In a real application, access to resources should be restricted.
             */
            Context cx = Context.newBuilder(JS).allowHostAccess(HostAccess.ALL).allowPolyglotAccess(PolyglotAccess.ALL)
                    .engine(sharedEngine).build();
            /*
             * Register a Java method in the Context global scope as a JavaScript function.
             */
            ContextProvider provider = new ContextProvider(cx);
            cx.getBindings(JS).putMember("computeFromJava", createJavaInteropComputeFunction(provider));
            System.out.println("Created new JS context for thread " + Thread.currentThread());
            return provider;
        });
    

    Note here the Context object can share the engine for efficiency, you probably want to do the same if your JS source is the same for every thread.

    Then when you process messages, every thread can retrieve its own Context and run message processing JavaScript in it.