Is Groovy ScriptingEngine thread safe?
Asked Answered
C

1

10

When you call :

 Object isThreadSafe = scriptEngine.getFactory().getParameter("THREADING");

It returns MULTITHREADED as per:

But it is not clear what is the exact impact of this.

Does it mean that :

  • I can call scriptEngine.eval(script, bindings); from different threads and that provided bindings is not shared it is thread safe ?
  • OR does it means there are some restriction on the script, and that as a consequence they should synchronize on any shared object ? . My understanding is that this seems to be the correct answer.
  • OR something else ?

It would be great if answers could illustrate with some code.

Capelin answered 6/2, 2018 at 14:1 Comment(0)
R
9

Unfortunately this information is misleading if it comes to GroovyScriptEngineImpl class. The Javadoc you mentioned says:

"MULTITHREADED" - The engine implementation is internally thread-safe and scripts may execute concurrently although effects of script execution on one thread may be visible to scripts on other threads.

GroovyScriptEngineImpl does not apply to this, because e.g. you can change the classloader with GroovyScriptEngineImpl.setClassLoader(GroovyClassLoader classLoader) method and it may cause unpredictable behavior when it happens in concurrent execution (this method is not even atomic and does not synchronize execution between threads).

Regarding scriptEngine.eval(script, bindings) execution, you have to be aware of its non-deterministic nature when you share same bindings across many different threads. javax.script.SimpleBindings default constructor uses HashMap and you should definitely avoid it - in case of multithreaded execution it's better to use ConcurrentHashMap<String,Object> to at least allow safe concurrent access. But even though you can't get any guarantee when you evaluate concurrently multiple scripts and those scripts will change global bindings. Consider following example:

import org.codehaus.groovy.jsr223.GroovyScriptEngineImpl

import javax.script.ScriptContext
import javax.script.SimpleBindings
import javax.script.SimpleScriptContext
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
import java.util.concurrent.Future

class GroovyScriptEngineExample {

    static void main(args) {
        def script1 = '''
def x = 4
y++
x++
'''

        def script2 = '''
def y = 10
x += y
'''

        final GroovyScriptEngineImpl engine = new GroovyScriptEngineImpl()
        final ExecutorService executorService = Executors.newFixedThreadPool(5)

        (0..3).each {
            List<Future> tasks = []

            final SimpleBindings bindings = new SimpleBindings(new ConcurrentHashMap<String, Object>())
            bindings.put('x', 1)
            bindings.put('y', 1)

            (0..<5).each {
                tasks << executorService.submit {
                    engine.setClassLoader(new GroovyClassLoader())
                    engine.eval(script1, bindings)
                }
                tasks << executorService.submit {
                    println engine.getClassLoader()
                    engine.eval(script2, bindings)
                }
            }

            tasks*.get()

            println bindings.entrySet()
        }

        executorService.shutdown()
    }
}

In this example we define two Groovy scripts:

def x = 4
y++
x++

and:

def y = 10
x += y

In the first script we define a local variable def x = 4 and x++ increments only our local script variable. When we print x binding after running this script we will see that it won't change during the execution. However y++ in this case incremented y binding value.

In the second script we define local variable def y = 10 and we add value of local y (10 in this case) to current global x binding value.

As you can see both scripts modify global bindings. In the exemplary code shown in this post we run both scripts 20 times concurrently. We have no idea in what order both scripts get executed (imagine that there is a random timeout in each execution, so one script may hang for couple of seconds). Our bindings use ConcurrentHashMap internally so we are only safe if it comes to concurrent access - two threads won't update same binding at the same time. But we have no idea what is the result. After each execution. The first level loop executes 4 times and internal loop executes 5 times and during each execution it submits script evaluation using shared script engine and shared bindings. Also first task replaces GroovyClassLoader in the engine to show you that it is not safe to share its instance across multiple threads. Below you can find exemplary output (exemplary, because each time you'll run there is a high probability you will get different results):

groovy.lang.GroovyClassLoader@1d6b34d4
groovy.lang.GroovyClassLoader@1d6b34d4
groovy.lang.GroovyClassLoader@64f061f1
groovy.lang.GroovyClassLoader@1c8107ef
groovy.lang.GroovyClassLoader@1c8107ef
[x=41, y=2]
groovy.lang.GroovyClassLoader@338f357a
groovy.lang.GroovyClassLoader@2bc966b6
groovy.lang.GroovyClassLoader@2bc966b6
groovy.lang.GroovyClassLoader@48469ff3
groovy.lang.GroovyClassLoader@48469ff3
[x=51, y=4]
groovy.lang.GroovyClassLoader@238fb21e
groovy.lang.GroovyClassLoader@798865b5
groovy.lang.GroovyClassLoader@17685149
groovy.lang.GroovyClassLoader@50d12b8b
groovy.lang.GroovyClassLoader@1a833027
[x=51, y=6]
groovy.lang.GroovyClassLoader@62e5f0c5
groovy.lang.GroovyClassLoader@62e5f0c5
groovy.lang.GroovyClassLoader@7c1f39b5
groovy.lang.GroovyClassLoader@657dc5d2
groovy.lang.GroovyClassLoader@28536260
[x=51, y=6]

A few conclusions:

  • replacing GroovyClassLoader is non-deterministic (in the 1st loop 3 different classloader instances were printed while in the 3rd one we have printed 5 different classloader instances)
  • final bindings calculation is non-deterministic. We have avoided concurrent write with ConcurrentHashMap but we have no control over the execution order, so in case of relying on binding value from previous execution you never know what value to expect.

So, how to be thread-safe when using GroovyScriptEngineImpl in multithreaded environment?

  • don't use global bindings
  • when using global bindings, make sure that scripts don't override bindings (you can use new SimpleBindings(Collections.unmodifiableMap(map)) for that
  • otherwise you have to accept non-deterministic nature of bindings state modification
  • extend GroovyScriptEngineImpl and don't allow changing classloader after object was initialized
  • otherwise accept that some other thread may mess up a little bit.

Hope it helps.

Regine answered 6/2, 2018 at 15:43 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.