oracle / graaljs

A ECMAScript 2023 compliant JavaScript implementation built on GraalVM. With polyglot language interoperability support. Running Node.js applications!
Universal Permissive License v1.0
1.77k stars 189 forks source link

Questions about graal.js #78

Closed tosehee75 closed 5 years ago

tosehee75 commented 5 years ago

Sorry if this is a dumb question. We are using the nashorn for the scripting needs, and heard that Graal is going to be replacing it. So I am evaluating how to migrate.

With nashorn, we implemented the rules engine where the expression can be evaluated using javascript runtime, and returns the value as part of the function call.

These function cannot take the expression as the expression itself is dynamic and different in runtime. So, we generate these function definitions, and then evaluate them every time.

Obviously, this is slow, but since nashorn doesn't support the multi-threading, we have no other option.

Now, my question is whether I run into the same issue with graal or not.

I read that Context must be created per thread and each thread can have one Context at a time.

This seems exactly like the nashorn. As long as I create the new ScriptEngine and evaluate every functions, then it works fine. However, if I create the CompiledScript, it either doesn't find the functions or the binding variables that I pass isn't available.

Here is the snippet of code that we are using with nashorn.

import javax.script.*;

public class NashornEngineCached {
    public static final Object NO_RESULT = new Object();
    private static final Cache<String, CacheScriptWrapper> CACHE = CacheBuilder.newBuilder().maximumSize(1000).build();

    private SimpleScriptContext bindings;
    private ScriptEngine engine;
    private DepRuleCacheable cacheable;
    private String appName;
    private String catName;
    private String subCatName;

    public void addToRuleContext(String key, Object value) {
        bindings.setAttribute(key, value, ScriptContext.ENGINE_SCOPE);
    }

    public boolean hasBinding(String key) {
        return bindings.getAttribute(key, ScriptContext.ENGINE_SCOPE) != null;
    }

    public void init(Context context, DepRuleCacheable cacheable, String appName, String catName, String subCatName) {
        javax.script.ScriptEngineManager engineManager = new javax.script.ScriptEngineManager();
        engine = engineManager.getEngineByName("nashorn");

        bindings = new SimpleScriptContext(); //engine.createBindings(); // engine.getBindings(ScriptContext.ENGINE_SCOPE);

        for (Parameter param : context.getNonTransientParameters()) {
            bindings.setAttribute(param.getName(), param.getValue(), ScriptContext.ENGINE_SCOPE);
        }

        bindings.setAttribute("_context", context, ScriptContext.ENGINE_SCOPE);
        bindings.setAttribute("applicationContext", SpringUtil.getInstance(), ScriptContext.ENGINE_SCOPE);

        this.cacheable = cacheable;
        this.appName = appName;
        this.catName = catName;
        this.subCatName = subCatName;

        try {
            findScript();
        }
        catch (ScriptException e) {
        }
    }

    private CompiledScript findScript() throws ScriptException {
        String schemaCode = (String) ThreadContext.get("CompanyCode");

        String url = schemaCode + "/" + appName + "/" + catName + "/" + subCatName;

        final CacheScriptWrapper wrapper = CACHE.getIfPresent(url);
        if (wrapper != null && wrapper.getVersion() == cacheable.getSubCat().getVersion()) {
            return wrapper.getCompiledScript();
        }

        Compilable compiler = Compilable.class.cast(engine);
        CompiledScript compiledScript = compiler.compile(cacheable.generateScriptContent());
        compiledScript.eval();

        CACHE.put(url, new CacheScriptWrapper(url, compiledScript, cacheable.getSubCat().getVersion()));

        return compiledScript;
    }

    public Object eval(String functionName, boolean hasReturn, String context, long contextId) {
        try {
            CompiledScript script = findScript();
            script.getEngine().setContext(bindings);
            script.eval();

            Invocable invocable = (Invocable) script.getEngine();

            if (hasReturn) {
                Object result = invocable.invokeFunction(functionName);

                return result;
            }
            else {
                invocable.invokeFunction(functionName);
            }

            return NO_RESULT;
        }
        catch (Throwable se) {
            String errorMsg = "Nashorn JavaScript Error [" + context + "/" + contextId + "]";

            throw new BaseRuntimeException(errorMsg, se);
        }
    }
}

And the script content that I generate looks something like this.

function subCatPreCondition_246() {return oh.shipToCountry != null;}

function condition_1280() {return oh.shipToCountry.equalsIgnoreCase("Saint Helena");}

In eval method, it finds the CompiledScript, then it sets the binding variables, and evaluate them. I am guessing that I can pass the data model into here, and it's accessible through its key name.

Then, I get the Invocable object, and invoke the function that I created above.

This works fine for a single thread. But if I run this across multiple threads, I get all sorts of odd behavior such as.

  1. It says the No Such Method found when looking up the function name.
  2. It can't find the bind variable being passed in.
  3. It evaluates the value against the wrong object. For example, if I run two threads, each with different order, it randomly assigns the wrong value to the wrong order.

To get around it, I am simply creating a new Engine, and evaluate the entire functions every time. It seems to work and have no issue. But again, it's darn slow... (up to 10 times slower than compiled script).

Is this sort of thing supported through graal?

tosehee75 commented 5 years ago

Put another way, is it possible for graal to share the common javascrit sources, and execute against different set of inputs through bindings?? And is this thread safe?

wirthi commented 5 years ago

Hi @tosehee75

I think you want to inspect http://www.graalvm.org/docs/graalvm-as-a-platform/embed/#enable-source-caching

This is not working via ScriptEngine, but our own Context API. This way you can share commonly used Source objects among several Contexts, have them being compiled (automatically) and then be fast for all the Contexts where they are used.

Best, Christian

tosehee75 commented 5 years ago

Does this mean i can share codes but pass different set of parameters without multi threading issue?

On Fri, Mar 8, 2019, 5:30 AM Christian Wirth notifications@github.com wrote:

Hi @tosehee75 https://github.com/tosehee75

I think you want to inspect http://www.graalvm.org/docs/graalvm-as-a-platform/embed/#enable-source-caching

This is not working via ScriptEngine, but our own Context API. This way you can share commonly used Source objects among several Contexts, have them being compiled (automatically) and then be fast for all the Contexts where they are used.

Best, Christian

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/graalvm/graaljs/issues/78#issuecomment-470881965, or mute the thread https://github.com/notifications/unsubscribe-auth/AYdgV5gJkTkT4twR9pCcpzHOxmPoWNRjks5vUjvCgaJpZM4Y8k_c .