Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

IllegalArgumentException exception from Nashorn - Is it a bug in Java 8?

I am using Nashorn javascript engine to evaluate all the server side javascript code written in a java application.In order improve performance I am using spring to initialize JsEngine while startup and evaluate & cache all the core tools such as Mustache and some common JS tools. Then each time when screen renders this pre-evaluated JsEngine will be used to evaluate page specific JavaScript code. It works fine for sometime, means it renders the page as expected but start throwing following exception when I keep hitting the same URL

I am not able to find the root cause of the issue.

@Component
public class JsEngine {

    private ScriptEngine scriptEngine;

    @PostConstruct
    public void init() throws ScriptException, IOException{
        scriptEngine = new ScriptEngineManager().getEngineByName("nashorn");
        this.cacheAllCoreEngines();
        for(String key: defaultEngineSource.keySet()){
            scriptEngine.eval(defaultEngineSource.get(key));
        }
    }

    private void cacheAllCoreEngines()throws IOException{
       //read all core files such as mustache, etc. 
       defaultEngineSource.put("mustache",  FileUtil.readFileFromDisk("<actual path..>/mustache.js"));
    }

    public Object eval(String source) throws  ScriptException{
            .... code to handle exceptions 
            return scriptEngine.eval (source);
    }

}

JsEngine used as given below,

public class AppRendererImpl implements  AppRenderer {

    @Autowired
    JsEngine jsEngine;

    public String render(){
     ....
     .... //Read source from disk or cache
     jsEngine.eval(source);....
    }     
}

Exception after few render cycles,

Exception:
java.lang.IllegalArgumentException: target and filter types do not match: (ScriptObject)Object, (Object)Object
at java.lang.invoke.MethodHandleStatics.newIllegalArgumentException(MethodHandleStatics.java:115)
at java.lang.invoke.MethodHandles.filterArgument(MethodHandles.java:2416)
at java.lang.invoke.MethodHandles.filterArguments(MethodHandles.java:2403)
at jdk.nashorn.internal.lookup.MethodHandleFactory$StandardMethodHandleFunctionality.filterArguments(MethodHandleFactory.java:277)
at jdk.nashorn.internal.runtime.WithObject.filter(WithObject.java:270)
at jdk.nashorn.internal.runtime.WithObject.fixExpressionCallSite(WithObject.java:249)
at jdk.nashorn.internal.runtime.WithObject.lookup(WithObject.java:169)
at jdk.nashorn.internal.runtime.linker.NashornLinker.getGuardedInvocation(NashornLinker.java:96)
at jdk.internal.dynalink.support.CompositeTypeBasedGuardingDynamicLinker.getGuardedInvocation(CompositeTypeBasedGuardingDynamicLinker.java:176)
at jdk.internal.dynalink.support.CompositeGuardingDynamicLinker.getGuardedInvocation(CompositeGuardingDynamicLinker.java:124)
at jdk.internal.dynalink.support.LinkerServicesImpl.getGuardedInvocation(LinkerServicesImpl.java:144)
at jdk.internal.dynalink.DynamicLinker.relink(DynamicLinker.java:232)
at jdk.nashorn.internal.scripts.Script$\^eval_._L6$_L8(:21)
at jdk.nashorn.internal.scripts.Script$\^eval_._L6$_L40(:41)
at jdk.nashorn.internal.scripts.Script$\^eval_.runScript(:1)
at jdk.nashorn.internal.runtime.ScriptFunctionData.invoke(ScriptFunctionData.java:498)
at jdk.nashorn.internal.runtime.ScriptFunction.invoke(ScriptFunction.java:206)
at jdk.nashorn.internal.runtime.ScriptRuntime.apply(ScriptRuntime.java:378)
at jdk.nashorn.api.scripting.NashornScriptEngine.evalImpl(NashornScriptEngine.java:546)
at jdk.nashorn.api.scripting.NashornScriptEngine.evalImpl(NashornScriptEngine.java:528)
at jdk.nashorn.api.scripting.NashornScriptEngine.evalImpl(NashornScriptEngine.java:524)
at jdk.nashorn.api.scripting.NashornScriptEngine.eval(NashornScriptEngine.java:194)
at javax.script.AbstractScriptEngine.eval(AbstractScriptEngine.java:264)
at com.nube.portal.engines.js.JsEngine.eval(JsEngine.java:111)
at com.nube.portal.engines.html.tags.HtmlTagScript.eval(HtmlTagScript.java:66)
........

I have some custom code added to copy all global objects to another map. This is to facilitate some other requirement I have to access all global object as "nube.". I don't know if this code will create any issues for frequent run. Please keep in mind I am not removing any objects from Context.

public void movePublicObjects(String prefix) throws NubeException{
    Bindings b1 = scriptEngine.getContext().getBindings(ScriptContext.ENGINE_SCOPE);
    Map<String, Object> nubeObjects = new HashMap<String, Object>();
    for(Entry<String, Object> entry: b1.entrySet()){
        if(!entry.getKey().equals("nube")){
            nubeObjects.put(entry.getKey(), entry.getValue());
        }
    }
    b1.put("nube", nubeObjects);
    return;
}

This code works perfectly when I define JsEngine as Prototype but performance is not good. Do you think this is a bug in Nashorn?

like image 774
kamoor Avatar asked Dec 07 '14 20:12

kamoor


2 Answers

IIRC, Spring @Component will default to singleton scope, so your JsEngine's single ScriptEngine instance will be shared across threads.

I found a discussion on the Nashorn developer mailing list regarding thread safety that had this to say:

. . . Nashorn is not thread safe by design. Indeed, if you evaluate

new NashornScriptEngineFactory().getParameter("THREADING")

it'll return null, which means "the engine implementation is not thread safe, and cannot be used to execute scripts concurrently on multiple threads" -- see http://docs.oracle.com/javase/7/docs/api/javax/script/ScriptEngineFactory.html#getParameter(java.lang.String)

Nashorn library internals themselves are thread safe . . . but JavaScript programs executing within a single engine instance are not thread safe.

This is different from Rhino, if you had tried that before. Using Nashorn, you probably need to take steps to protect your ScriptEngine from concurrent access, which might explain the unpredictable behavior you observe.

Consider setting up a pool, access engine instances that are thread-local, or change your component scope to prototype instead of singleton.


Here's a potential solution using thread-local instances of initialized script engines:

@Component
public class JsEngine {

    private final ThreadLocal<ScriptEngine> threadEngines =
        new ThreadLocal<ScriptEngine>() {
            @Override
            protected ScriptEngine initialValue() {
                ScriptEngine engine =
                    new ScriptEngineManager().getEngineByName("nashorn");
                for (String key: defaultEngineSource.keySet()) {
                    engine.eval(defaultEngineSource.get(key));
                }
                return engine;
            }
        };

    @PostConstruct
    public void init() throws ScriptException, IOException {
        this.cacheAllCoreEngines();
        // engine initialization moved to per-thread via ThreadLocal
    }

    private void cacheAllCoreEngines() throws IOException{
       //read all core files such as mustache, etc. 
       defaultEngineSource.put("mustache", FileUtil.readFileFromDisk("<actual path..>/mustache.js"));
    }

    public Object eval(String source) throws ScriptException{
        // .... code to handle exceptions 
        return threadEngines.get().eval(source);
    }
}

There will still be overhead for initializing your engine (with the mustache, etc., code) for each new thread that uses your component. However, if your application is using a thread pool then those same threads should be reused and you won't pay that cost on every invocation as you would with a prototype scope.

like image 108
William Price Avatar answered Oct 06 '22 00:10

William Price


Either you damage something in the persistent JS context which then violates the optimizer's assumptions, or this is a flat-out bug. That stack trace shows that your code went to invoke a method and the code path within the scripting runtime managed to reach a spot where an optimized version of the function is about to be applied to arguments which do not match its assumptions (its argument type filters) so it fails with this exception. That code path should never be followed in a consistent runtime, regardless of your JS code.

like image 32
Marko Topolnik Avatar answered Oct 06 '22 01:10

Marko Topolnik