0

I have a scenario where I am processing user provided templates that contain portions of JavaScript. I am using Java's ScriptEngine to evaluate those bits of JavaScript. I have various helper functions that I only wish to evaluate once (think of these helpers functions as loading something like Underscore.js).

Once this has been loaded I then loop through rows of data and process the template for each row - these templates may use any of the previously loaded helper functions. I am trying to process the rows in parallel as Java's ScriptEngine.eval() is quite slow in this scenario.

Anyone who looks at this over simplified SSCCE will know that I am getting the expected outcome that I have shown, as I am modifying a global object in a non thread safe way. In my example sJsGlobal is this global JavaScript. The global functions need access to various row specific bits of data, that I place within obj.

For sJsGlobal to successfully eval() it needs to know about obj or it will fail. In my SSCCE, each thread is updating the same global obj before calling a function. I am looking for a way for each global function to see a local obj. It is a Catch-22 situation, I need to define the variable for the global script to know it exists, but I want it to be of local significance.

import java.util.*;
import java.util.concurrent.*;
import javax.script.*;

public class SSCCE {
  public static void main(String[] args) throws Exception {
    StringBuilder sJsGlobal = new StringBuilder();
    sJsGlobal.append("var obj = {};");
    sJsGlobal.append("function get_row() { return \"Row is \" + obj.row }"); 

    ScriptEngine jsGlobal = new ScriptEngineManager().getEngineByName("JavaScript");
    jsGlobal.eval(sJsGlobal.toString());

    Bindings gB = jsGlobal.getBindings(ScriptContext.ENGINE_SCOPE);

    ExecutorService es = Executors.newCachedThreadPool();
    ArrayList<Future<String>> af = new ArrayList<Future<String>>();

    for (int i = 0; i < 10; i++) {
      final int fi = i;
      af.add(es.submit(() -> {
        StringBuilder sJsLocal = new StringBuilder();
        sJsLocal.append("obj.row = " + fi + ";");
        sJsLocal.append("var x = get_row();");

        ScriptEngine jsLocal = new ScriptEngineManager().getEngineByName("JavaScript");
        ScriptContext sc = new SimpleScriptContext();
        Bindings lB = jsLocal.createBindings();
        lB.putAll(gB);
        sc.setBindings(lB, ScriptContext.ENGINE_SCOPE);
        jsLocal.setContext(sc);

        jsLocal.eval(sJsLocal.toString());
        return (String)jsLocal.get("x");
      }));
    }

    for (Future<String> f : af) {
      System.out.println("Returned -> " + f.get());
    }
    es.shutdown();
  }
}

This results in the following output where the rows are being overwritten as it isn't thread safe due to my use of this global obj variable. Even with a separate ScriptEngineManager and ScriptEngine. I need a way of settings the bindings so they don't interfere with other instances.

Returned -> Row is 4
Returned -> Row is 4
Returned -> Row is 4
Returned -> Row is 4
Returned -> Row is 8
Returned -> Row is 5
Returned -> Row is 6
Returned -> Row is 7
Returned -> Row is 8
Returned -> Row is 9

I could pass the row specific data into each function as variables, but this becomes very messy as my example doesn't do justice to the amount of data I need to exchange. The functions are also called by user scripts which makes this very difficult. I really want to find a way to redefine obj in each thread before I evaluate sJsLocal.

This all works fine if I process each row one after another without threading, but I am trying to speed this up a bit.

chrixm
  • 942
  • 6
  • 26

1 Answers1

0

After a lot of trial, error and effort and reading lots of similar questions with no answers, I have come up with the following which seems to work and is documented here for anyone else who comes across this problem. This uses a common ScriptEngine with an individual ScriptContext per thread to provide isolation.

import java.util.*;
import java.util.concurrent.*;
import javax.script.*;

public class SSCCE {
  public static void main(String[] args) throws Exception {
    ScriptEngine js = new ScriptEngineManager().getEngineByName("JavaScript");

    StringBuilder sJsGlobal = new StringBuilder();
    sJsGlobal.append("var obj = {};");
    sJsGlobal.append("function get_row() { return \"Row is \" + obj.row; }"); 
    CompiledScript jsLib = ((Compilable)js).compile(sJsGlobal.toString());

    ExecutorService es = Executors.newCachedThreadPool();
    ArrayList<Future<String>> af = new ArrayList<Future<String>>();

    for (int i = 0; i < 10; i++) {
      final int fi = i;
      af.add(es.submit(() -> {
        StringBuilder sJsLocal = new StringBuilder();
        sJsLocal.append("obj.row = " + fi + ";");
        sJsLocal.append("var x = get_row();");

        ScriptContext scLocal = new SimpleScriptContext();
        scLocal.setBindings(js.createBindings(), ScriptContext.ENGINE_SCOPE);
        jsLib.eval(scLocal);
        js.eval(sJsLocal.toString(), scLocal);
        return (String)scLocal.getAttribute("x");
      }));
    }

    for (Future<String> f : af) {
      System.out.println("Returned -> " + f.get());
    }
    es.shutdown();
  }
}

This results in the following when run:

Returned -> Row is 0
Returned -> Row is 1
Returned -> Row is 2
Returned -> Row is 3
Returned -> Row is 4
Returned -> Row is 5
Returned -> Row is 6
Returned -> Row is 7
Returned -> Row is 8
Returned -> Row is 9
chrixm
  • 942
  • 6
  • 26