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)
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:
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.