javathread-safetygroovyshell

GroovyShell Thread safety


The question appears in comments of all questions about GroovyShell, like Using GroovyShell as "expression evaluator/engine" (or: How to reuse GroovyShell). Not a surprise, since the design of the API seems it's not covering this topic at all. Unfortunately, this was never discussed explicitly.

The problem in a compact form:

Static initialization:

final GroovyShell shell = new GroovyShell();
final Script thisScript = shell.parse("sleep 1000; return input1+' '+input2");
//anotherScript = // not relevant here but my use-case pre-loads ~300 groovy scripts

script runner:

private Object runScript(Script theScript, String param1, String param2) {
  theScript.setProperty("input1", param1);
  theScript.setProperty("input2", param2);
  Object result = theScript.run();
  return result;
}

serialized execution:

runScript(thisScript, "Hello", "World")   -> Hello World
runScript(thisScript, "Guten", "Tag")     -> Guten Tag

parallel execution:

runScript(thisScript, "Hello", "World")   -> Guten Tag (!)
runScript(thisScript, "Guten", "Tag")     -> Guten Tag

The problem is that the binding (no matter if get/setBinding or setProperty) is done on script level. That's like setting something on a loaded java.lang.Class object after loading it through the classLoader or modifying static member variables. Is there an alternative groovy implementation which handles binding and running as an atomic operation? Or even better: uses a context object for execution?

The simplest workaround is synchronizing runScript() to the script object but this doesn't scale.


Solution

  • create different instances of script class to run them in parallel.

    GroovyShell shell = new GroovyShell();
    Class<Script> scriptClass = shell.parse("sleep 1000; return input1+' '+input2").getClass();
    
    Object runScript(Class<Script> clazz, String param1, String param2) {
        Script theScript = clazz.newInstance();
        theScript.setProperty("input1", param1);
        theScript.setProperty("input2", param2);
        Object result = theScript.run();
        return result;
    }
    //thread test
    [
        ["111","aaa"],
        ["222","bbb"]
    ].collect{x->
        Thread.start{
            println "start $x"
            println runScript(scriptClass, x[0], x[1])
        }
    }*.join()
    

    output:

    start [111, aaa]
    start [222, bbb]
    111 aaa
    222 bbb