Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Interrupt java thread running nashorn script

In the code below i have javascript running in a separate thread from the main one. That script is an infinite loop, so it needs to be terminated somehow. How?

Calling .cancel() is not working AFTER the script begins running. But if i call .cancel() just after the thread initialization, it will terminate it (the commented out line).

package testscriptterminate;

import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

import java.util.Timer;
import java.util.TimerTask;

public class TestScriptTerminate extends TimerTask{

    private ExecutorService threads;
    private Future runScript;

    private Timer t;

    public TestScriptTerminate(){
        t = new Timer();
        t.schedule(this, 6000); //let the script run for a while before attempt to cancel

        threads = Executors.newFixedThreadPool(1);
        runScript = threads.submit(new ScriptExec());

        //runScript.cancel(true); //will cancel here, before the script had a change to run, but useless, i want to cancel at any time on demand
    }

    @Override
    public void run(){
        //after script has fully initialized and ran for a while - attempt to cancel.
        //DOESN'T WORK, thread still active
        System.out.println("Canceling now...");
        runScript.cancel(true);
    }

    public static void main(String[] args) {
        new TestScriptTerminate();
    }


}

class ScriptExec implements Runnable{

    private ScriptEngine js;
    private ScriptEngineManager scriptManager;

    public ScriptExec(){
        init();
    }

    @Override
    public void run() {
        try {
            js.eval("while(true){}");
        } catch (ScriptException ex) {
            System.out.println(ex.toString());
        }
    }

    private void init(){
        scriptManager = new ScriptEngineManager();
        js = scriptManager.getEngineByName("nashorn");
    }
}
like image 374
user2092119 Avatar asked Jul 20 '14 21:07

user2092119


2 Answers

So this is old, but i just wrote this up and thought it would probably be valuable to share. By default there is ~nothing you can do to stop a Nashorn script executing, .cancel() Thread.stop() Thread.interrupt() do nothing, but if you are willing to put in a bit of effort and are ok with rewriting some bytecode, it is achieveable. Details:

http://blog.getsandbox.com/2018/01/15/nashorn-interupt/

like image 110
Nick Avatar answered Oct 20 '22 05:10

Nick


JavaScript (under Nashorn), like Java, will not respond to an interrupt in the middle of a tight loop. The script needs to poll for interruption and terminate the loop voluntarily, or it can call something that checks for interruption and let InterruptedException propagate.

You might think that Nashorn is "just running a script" and that it should be interrupted immediately. This doesn't apply, for the same reason that it doesn't apply in Java: asynchronous interruption risks corruption of the application's data structures, and there is essentially no way to avoid it or recover from it.

Asynchronous interruption brings in the same problems as the long-deprecated Thread.stop method. This is explained in this document, which is an updated version of the document linked in the comments.

Java Thread Primitive Deprecation

See also Goetz, Java Concurrency In Practice, Chapter 7, Cancellation and Shutdown.

The easiest way to check for interruption is to call Thread.interrupted(). You can call this quite easily from JavaScript. Here's a rewrite of the example program that cancels the running script after five seconds:

public class TestScriptTerminate {

    ScheduledExecutorService pool = Executors.newScheduledThreadPool(2);

    void script() {
        ScriptEngineManager scriptManager = new ScriptEngineManager();
        ScriptEngine js = scriptManager.getEngineByName("nashorn");
        try {
            System.out.println("Script starting.");
            js.eval("while (true) { if (java.lang.Thread.interrupted()) break; }");
            System.out.println("Script finished.");
        } catch (ScriptException ex) {
            ex.printStackTrace();
        }
    }

    void init() throws Exception {
        Future<?> scriptTask = pool.submit(this::script);
        pool.schedule(() -> {
            System.out.println("Canceling now...");
            scriptTask.cancel(true);
        }, 5, TimeUnit.SECONDS);
        pool.shutdown();
    }

    public static void main(String[] args) throws Exception {
        new TestScriptTerminate().init();
    }
}

Since we're starting up a thread pool, might as well make it a scheduled thread pool so that we can use it for both the script task and the timeout. That way we can avoid Timer and TimerTask, which are mostly replaced by ScheduledExecutorService anyway.

The usual convention when handling and interrupt is either to restore the interrupt bit or to let an InterruptedException propagate. (One should never ignore an interrupt.) Since breaking out of the loop can be considered to have completed the handling of the interrupt, neither is necessary, and it seems sufficient simply to let the script exit normally.

This rewrite also moves a lot of work out of the constructor into an init() method. This prevents the instance from being leaked to other threads from within the constructor. There is no obvious danger from this in the original example code -- in fact, there almost never is -- but it's always good practice to avoid leaking the instance from the constructor.

like image 24
Stuart Marks Avatar answered Oct 20 '22 04:10

Stuart Marks