Porting From Nashorn: How to Handle JS Multi-Threading on GraalVM

Andreas Müller
The Startup
Published in
3 min readJul 8, 2020

Porting existing JavaScript code from Nashorn to GraalVM can be a challenge. This article focus on a multi-threading issue we got into when we ported SwiftMQ Stream scripts generated from Flow Director.

Multi-threading JavaScript on GraalVM

JavaScript is single-threaded

If you register an asynchronous callback on a Java class and this callback is invoked while you are still within the execution of the same JS script, you have multi-threaded access, even if you immediately schedule the processing of the callback in an event queue. Nashorn doesn’t check this, but GraalVM does and throws an exception. So any script with such callbacks will not work on GraalVM.

The asynchronous Callback Problem

Here is an example of registering an asynchronous message listener on a RabbitMQ channel:

var MYCONSUMER = Java.extend(Java.type("com.rabbitmq.client.DefaultConsumer"), 
{
handleDelivery: function (consumerTag, envelope,
properties, body) {
// Add it to an event queue (skipped)
}
});

this.channel.basicConsume(this.declareOk.getQueue(),
false,
this.consumerTag,
new MYCONSUMER(this.channel));

It receives a message and immediately enqueues it into an event queue that all other components in this script are using too to ensure synchronous access. So everything is running out of this event queue — except the asynchronous function call handleDelivery which is invoked from a RabbitMQ thread.

It works on Nashorn but not on GraalVM because GraalVM wraps any JS code with a Polyglot Context and checks access by calling Context.enter() before and Context.leave() after executing the code. During Context.enter(), it throws an exception when it detects multi-threaded access.

The developers of GraalVM are fully aware of this problem and even published recommendations on how it can be solved. But none of them works for us because we initiate everything out of the JS scripts including registering asynchronous callbacks. Wrapping each Java library that we use with our classes to enqueue asynchronous calls into the event queue wasn’t an option.

How to solve it

How can we get in-between the call from the Java thread and the JS callback invocation to invoke it from our event queue?

There is Java’s Proxy class to the rescue!

With that, you can dynamically create objects that implement all interfaces of a source object, act as an instance of it, and allows interception of method calls. Instead of the JS callback, a proxy registers as a callback, and we can intercept the calls into JS before it happens, thus avoiding this Context.enter()/leave() and execute the call from our synchronous event queue.

The AsyncProxy

Below is our proxy class to intercept the callbacks:

public class AsyncProxy 
implements java.lang.reflect.InvocationHandler {

private Object obj;
private Stream stream;
private int hashcode;
private String toString;

private AsyncProxy(Stream stream, Object obj) {
this.stream = stream;
this.obj = obj;
this.hashcode = obj.hashCode();
this.toString = obj.toString();
}

public static Object newInstance(Stream stream,
String interfaceClassName,
Object obj) throws Exception {
return java.lang.reflect.Proxy.newProxyInstance(
stream.getStreamCtx().classLoader,
new Class[]{
stream
.getStreamCtx()
.classLoader
.loadClass(interfaceClassName)
},
new AsyncProxy(stream, obj));
}

@Override
public Object invoke(Object proxy,
Method method,
Object[] args) throws Throwable {
// Exec it on our event queue
stream.getStreamCtx().streamProcessor
.dispatch(new POExecute(null, () -> {
try {
method.invoke(obj, args);
} catch (Exception e) {
stream.getStreamCtx().logStackTrace(e);
}
}));
return null;
}

@Override
public int hashCode() {
return hashcode;
}

@Override
public boolean equals(Object obj) {
return obj.equals(this.obj);
}

@Override
public String toString() {
return toString;
}
}

The important part is this:

new Class[]{
stream
.getStreamCtx()
.classLoader
.loadClass(interfaceClassName)
}

The typical examples of using the Proxy class acquire the name of the interfaces from the object. It will not work because, in that case, it uses the JS adapter class. It will only work if we directly load the original Java interface class and create a proxy. Here, the proxy directly sits at the registration class, and that’s what we want.

Connect it to JS

In SwiftMQ Streams, each JS script has a predefined environment. Part of it is a Stream Java class that is registered at the scripting context and accessible from the JS script via the stream variable.

So we added a method to mark any object as asynchronous and wrap it with a proxy:

public Object async(String interfaceClassName, Object callback) throws Exception {
return AsyncProxy.newInstance(this,
interfaceClassName,
callback);
}

The JS side then uses it to wrap a callback:

this.channel.basicConsume(this.declareOk.getQueue(), 
false, this.consumerTag,
stream.async(
"com.rabbitmq.client.Consumer",
new MYCONSUMER(this.channel)
));

And we are done!

Here is a GitHub repo with a self-contained example. I wish you happy multi-threading!

--

--

Andreas Müller
The Startup

Andreas is a well-known messaging expert, creator of SwiftMQ, CEO of swiftmq.com and CEO/CTO of flowdirector.io.