org.apache.tinkerpop.gremlin.groovy.engine.GremlinExecutorTest.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.tinkerpop.gremlin.groovy.engine.GremlinExecutorTest.java

Source

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package org.apache.tinkerpop.gremlin.groovy.engine;

import org.apache.commons.lang3.concurrent.BasicThreadFactory;
import org.apache.tinkerpop.gremlin.TestHelper;
import org.apache.tinkerpop.gremlin.groovy.jsr223.customizer.ThreadInterruptCustomizerProvider;
import org.apache.tinkerpop.gremlin.groovy.jsr223.customizer.TimedInterruptCustomizerProvider;
import org.apache.tinkerpop.gremlin.groovy.jsr223.customizer.TimedInterruptTimeoutException;
import org.javatuples.Pair;
import org.junit.Test;

import javax.script.Bindings;
import javax.script.CompiledScript;
import javax.script.SimpleBindings;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.IntStream;

import static org.hamcrest.CoreMatchers.instanceOf;
import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.greaterThan;
import static org.hamcrest.core.Is.is;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

/**
 * @author Stephen Mallette (http://stephen.genoprime.com)
 */
public class GremlinExecutorTest {
    public static Map<String, String> PATHS = new HashMap<>();
    private final BasicThreadFactory testingThreadFactory = new BasicThreadFactory.Builder()
            .namingPattern("test-gremlin-executor-%d").build();

    static {
        try {
            final List<String> groovyScriptResources = Collections.singletonList("GremlinExecutorInit.groovy");
            for (final String fileName : groovyScriptResources) {
                PATHS.put(fileName, TestHelper.generateTempFileFromResource(GremlinExecutorTest.class, fileName, "")
                        .getAbsolutePath());
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Test
    public void shouldRaiseExceptionInWithResultOfLifeCycle() throws Exception {
        final GremlinExecutor gremlinExecutor = GremlinExecutor.build().create();
        final GremlinExecutor.LifeCycle lc = GremlinExecutor.LifeCycle.build().withResult(r -> {
            throw new RuntimeException("no worky");
        }).create();

        final AtomicBoolean exceptionRaised = new AtomicBoolean(false);

        final CompletableFuture<Object> future = gremlinExecutor.eval("1+1", "gremlin-groovy", new SimpleBindings(),
                lc);
        future.handle((r, t) -> {
            exceptionRaised.set(t != null && t instanceof RuntimeException && t.getMessage().equals("no worky"));
            return null;
        }).get();

        assertThat(exceptionRaised.get(), is(true));

        gremlinExecutor.close();
    }

    @Test
    public void shouldEvalScript() throws Exception {
        final GremlinExecutor gremlinExecutor = GremlinExecutor.build().create();
        assertEquals(2, gremlinExecutor.eval("1+1").get());
        gremlinExecutor.close();
    }

    @Test
    public void shouldCompileScript() throws Exception {
        final GremlinExecutor gremlinExecutor = GremlinExecutor.build().create();
        final CompiledScript script = gremlinExecutor.compile("1+1").get();
        assertEquals(2, script.eval());
        gremlinExecutor.close();
    }

    @Test
    public void shouldEvalSuccessfulAssertionScript() throws Exception {
        final GremlinExecutor gremlinExecutor = GremlinExecutor.build().create();
        assertNull(gremlinExecutor.eval("assert 1==1").get());
        gremlinExecutor.close();
    }

    @Test
    public void shouldEvalFailingAssertionScript() throws Exception {
        try (GremlinExecutor gremlinExecutor = GremlinExecutor.build().create()) {
            gremlinExecutor.eval("assert 1==0").get();
            fail("Should have thrown an exception");
        } catch (Exception ex) {
            assertThat(ex.getCause(), instanceOf(AssertionError.class));
        }
    }

    @Test
    public void shouldEvalMultipleScripts() throws Exception {
        final GremlinExecutor gremlinExecutor = GremlinExecutor.build().create();
        assertEquals(2, gremlinExecutor.eval("1+1").get());
        assertEquals(3, gremlinExecutor.eval("1+2").get());
        assertEquals(4, gremlinExecutor.eval("1+3").get());
        assertEquals(5, gremlinExecutor.eval("1+4").get());
        assertEquals(6, gremlinExecutor.eval("1+5").get());
        assertEquals(7, gremlinExecutor.eval("1+6").get());
        gremlinExecutor.close();
    }

    @Test
    public void shouldEvalScriptWithBindings() throws Exception {
        final GremlinExecutor gremlinExecutor = GremlinExecutor.build().create();
        final Bindings b = new SimpleBindings();
        b.put("x", 1);
        assertEquals(2, gremlinExecutor.eval("1+x", b).get());
        gremlinExecutor.close();
    }

    @Test
    public void shouldEvalScriptWithMapBindings() throws Exception {
        final GremlinExecutor gremlinExecutor = GremlinExecutor.build().create();
        final Map<String, Object> b = new HashMap<>();
        b.put("x", 1);
        assertEquals(2, gremlinExecutor.eval("1+x", b).get());
        gremlinExecutor.close();
    }

    @Test
    public void shouldEvalScriptWithMapBindingsAndLanguage() throws Exception {
        final GremlinExecutor gremlinExecutor = GremlinExecutor.build().addEngineSettings("nashorn",
                Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), Collections.emptyMap())
                .create();
        final Map<String, Object> b = new HashMap<>();
        b.put("x", 1);
        assertEquals(2.0, gremlinExecutor.eval("1+x", "nashorn", b).get());
        gremlinExecutor.close();
    }

    @Test
    public void shouldEvalScriptWithMapBindingsAndLanguageThenTransform() throws Exception {
        final GremlinExecutor gremlinExecutor = GremlinExecutor.build().addEngineSettings("nashorn",
                Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), Collections.emptyMap())
                .create();
        final Map<String, Object> b = new HashMap<>();
        b.put("x", 1);
        assertEquals(4, gremlinExecutor.eval("1+x", "nashorn", b, r -> ((Double) r).intValue() * 2).get());
        gremlinExecutor.close();
    }

    @Test
    public void shouldEvalScriptWithMapBindingsAndLanguageThenConsume() throws Exception {
        final GremlinExecutor gremlinExecutor = GremlinExecutor.build().addEngineSettings("nashorn",
                Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), Collections.emptyMap())
                .create();
        final Map<String, Object> b = new HashMap<>();
        b.put("x", 1);

        final CountDownLatch latch = new CountDownLatch(1);
        final AtomicInteger result = new AtomicInteger(0);
        assertEquals(2.0, gremlinExecutor.eval("1+x", "nashorn", b, r -> {
            result.set(((Double) r).intValue() * 2);
            latch.countDown();
        }).get());

        latch.await();
        assertEquals(4, result.get());
        gremlinExecutor.close();
    }

    @Test
    public void shouldEvalScriptWithGlobalBindings() throws Exception {
        final Bindings b = new SimpleBindings();
        b.put("x", 1);
        final GremlinExecutor gremlinExecutor = GremlinExecutor.build().globalBindings(b).create();
        assertEquals(2, gremlinExecutor.eval("1+x").get());
        gremlinExecutor.close();
    }

    @Test
    public void shouldGetGlobalBindings() throws Exception {
        final Bindings b = new SimpleBindings();
        final Object bound = new Object();
        b.put("x", bound);
        final GremlinExecutor gremlinExecutor = GremlinExecutor.build().globalBindings(b).create();
        assertEquals(bound, gremlinExecutor.getGlobalBindings().get("x"));
        gremlinExecutor.close();
    }

    @Test
    public void shouldEvalScriptWithGlobalAndLocalBindings() throws Exception {
        final Bindings g = new SimpleBindings();
        g.put("x", 1);
        final GremlinExecutor gremlinExecutor = GremlinExecutor.build().globalBindings(g).create();
        final Bindings b = new SimpleBindings();
        b.put("y", 1);
        assertEquals(2, gremlinExecutor.eval("y+x", b).get());
        gremlinExecutor.close();
    }

    @Test
    public void shouldEvalScriptWithLocalOverridingGlobalBindings() throws Exception {
        final Bindings g = new SimpleBindings();
        g.put("x", 1);
        final GremlinExecutor gremlinExecutor = GremlinExecutor.build().globalBindings(g).create();
        final Bindings b = new SimpleBindings();
        b.put("x", 10);
        assertEquals(11, gremlinExecutor.eval("x+1", b).get());
        gremlinExecutor.close();
    }

    @Test
    public void shouldTimeoutSleepingScript() throws Exception {
        final AtomicBoolean successCalled = new AtomicBoolean(false);
        final AtomicBoolean failureCalled = new AtomicBoolean(false);

        final CountDownLatch timeOutCount = new CountDownLatch(1);

        final GremlinExecutor gremlinExecutor = GremlinExecutor.build().scriptEvaluationTimeout(250)
                .afterFailure((b, e) -> failureCalled.set(true)).afterSuccess((b) -> successCalled.set(true))
                .afterTimeout((b) -> timeOutCount.countDown()).create();
        try {
            gremlinExecutor.eval("Thread.sleep(1000);10").get();
            fail("This script should have timed out with an exception");
        } catch (Exception ex) {
            assertEquals(TimeoutException.class, ex.getCause().getClass());
        }

        assertTrue(timeOutCount.await(2000, TimeUnit.MILLISECONDS));

        assertFalse(successCalled.get());
        assertFalse(failureCalled.get());
        assertEquals(0, timeOutCount.getCount());
        gremlinExecutor.close();
    }

    @Test
    public void shouldOverrideBeforeEval() throws Exception {
        final AtomicInteger called = new AtomicInteger(0);
        final GremlinExecutor gremlinExecutor = GremlinExecutor.build().beforeEval(b -> called.set(1)).create();
        assertEquals(2, gremlinExecutor.eval("1+1", null, new SimpleBindings(),
                GremlinExecutor.LifeCycle.build().beforeEval(b -> called.set(200)).create()).get());

        // need to wait long enough for the callback to register
        Thread.sleep(500);

        assertEquals(200, called.get());
    }

    @Test
    public void shouldOverrideAfterSuccess() throws Exception {
        final AtomicInteger called = new AtomicInteger(0);
        final GremlinExecutor gremlinExecutor = GremlinExecutor.build().afterSuccess(b -> called.set(1)).create();
        assertEquals(2,
                gremlinExecutor
                        .eval("1+1", null, new SimpleBindings(),
                                GremlinExecutor.LifeCycle.build().afterSuccess(b -> called.set(200)).create())
                        .get());

        // need to wait long enough for the callback to register
        Thread.sleep(500);

        assertEquals(200, called.get());
    }

    @Test
    public void shouldOverrideAfterFailure() throws Exception {
        final AtomicInteger called = new AtomicInteger(0);
        final GremlinExecutor gremlinExecutor = GremlinExecutor.build().afterFailure((b, t) -> called.set(1))
                .create();
        try {
            gremlinExecutor
                    .eval("10/0", null, new SimpleBindings(),
                            GremlinExecutor.LifeCycle.build().afterFailure((b, t) -> called.set(200)).create())
                    .get();
            fail("Should have failed with division by zero");
        } catch (Exception ignored) {

        }

        // need to wait long enough for the callback to register
        Thread.sleep(500);

        assertEquals(200, called.get());
    }

    @Test
    public void shouldCallFail() throws Exception {
        final AtomicBoolean timeoutCalled = new AtomicBoolean(false);
        final AtomicBoolean successCalled = new AtomicBoolean(false);
        final AtomicBoolean failureCalled = new AtomicBoolean(false);
        final GremlinExecutor gremlinExecutor = GremlinExecutor.build()
                .afterFailure((b, e) -> failureCalled.set(true)).afterSuccess((b) -> successCalled.set(true))
                .afterTimeout((b) -> timeoutCalled.set(true)).create();
        try {
            gremlinExecutor.eval("10/0").get();
            fail();
        } catch (Exception ignored) {
        }

        // need to wait long enough for the callback to register
        Thread.sleep(500);

        assertFalse(timeoutCalled.get());
        assertFalse(successCalled.get());
        assertTrue(failureCalled.get());
        gremlinExecutor.close();
    }

    @Test
    public void shouldCallSuccess() throws Exception {
        final AtomicBoolean timeoutCalled = new AtomicBoolean(false);
        final AtomicBoolean successCalled = new AtomicBoolean(false);
        final AtomicBoolean failureCalled = new AtomicBoolean(false);
        final GremlinExecutor gremlinExecutor = GremlinExecutor.build()
                .afterFailure((b, e) -> failureCalled.set(true)).afterSuccess((b) -> successCalled.set(true))
                .afterTimeout((b) -> timeoutCalled.set(true)).create();
        assertEquals(2, gremlinExecutor.eval("1+1").get());

        // need to wait long enough for the callback to register
        Thread.sleep(500);

        assertFalse(timeoutCalled.get());
        assertTrue(successCalled.get());
        assertFalse(failureCalled.get());
        gremlinExecutor.close();
    }

    @Test
    public void shouldEvalInMultipleThreads() throws Exception {
        final GremlinExecutor gremlinExecutor = GremlinExecutor.build().create();

        final CyclicBarrier barrier = new CyclicBarrier(2);
        final AtomicInteger i1 = new AtomicInteger(0);
        final AtomicBoolean b1 = new AtomicBoolean(false);
        final Thread t1 = new Thread(() -> {
            try {
                barrier.await();
                i1.set((Integer) gremlinExecutor.eval("1+1").get());
            } catch (Exception ex) {
                b1.set(true);
            }
        });

        final AtomicInteger i2 = new AtomicInteger(0);
        final AtomicBoolean b2 = new AtomicBoolean(false);
        final Thread t2 = new Thread(() -> {
            try {
                barrier.await();
                i2.set((Integer) gremlinExecutor.eval("1+1").get());
            } catch (Exception ex) {
                b2.set(true);
            }
        });

        t1.start();
        t2.start();

        t1.join();
        t2.join();

        assertEquals(2, i1.get());
        assertEquals(2, i2.get());
        assertFalse(b1.get());
        assertFalse(b2.get());

        gremlinExecutor.close();
    }

    @Test
    public void shouldNotExhaustThreads() throws Exception {
        final ScheduledExecutorService executorService = Executors.newScheduledThreadPool(2, testingThreadFactory);
        final GremlinExecutor gremlinExecutor = GremlinExecutor.build().executorService(executorService)
                .scheduledExecutorService(executorService).create();

        final AtomicInteger count = new AtomicInteger(0);
        assertTrue(IntStream.range(0, 1000).mapToObj(i -> gremlinExecutor.eval("1+1")).allMatch(f -> {
            try {
                return (Integer) f.get() == 2;
            } catch (Exception ex) {
                throw new RuntimeException(ex);
            } finally {
                count.incrementAndGet();
            }
        }));

        assertEquals(1000, count.intValue());

        executorService.shutdown();
        executorService.awaitTermination(30000, TimeUnit.MILLISECONDS);
    }

    @Test
    public void shouldFailUntilImportExecutes() throws Exception {
        final GremlinExecutor gremlinExecutor = GremlinExecutor.build().create();

        final Set<String> imports = new HashSet<String>() {
            {
                add("import java.awt.Color");
            }
        };

        final AtomicInteger successes = new AtomicInteger(0);
        final AtomicInteger failures = new AtomicInteger(0);

        // issue 1000 scripts in one thread using a class that isn't imported.  this will result in failure.
        // while that thread is running start a new thread that issues an addImports to include that class.
        // this should block further evals in the first thread until the import is complete at which point
        // evals in the first thread will resume and start to succeed
        final Thread t1 = new Thread(
                () -> IntStream.range(0, 1000).mapToObj(i -> gremlinExecutor.eval("Color.BLACK")).forEach(f -> {
                    f.exceptionally(t -> failures.incrementAndGet()).join();
                    if (!f.isCompletedExceptionally())
                        successes.incrementAndGet();
                }));

        final Thread t2 = new Thread(() -> {
            while (failures.get() < 500) {
            }
            gremlinExecutor.getScriptEngines().addImports(imports);
        });

        t1.start();
        t2.start();

        t1.join();
        t2.join();

        assertTrue(successes.intValue() > 0);
        assertTrue(failures.intValue() >= 500);

        gremlinExecutor.close();
    }

    @Test
    public void shouldInitializeWithScript() throws Exception {
        final GremlinExecutor gremlinExecutor = GremlinExecutor.build()
                .addEngineSettings("gremlin-groovy", Collections.emptyList(), Collections.emptyList(),
                        Arrays.asList(PATHS.get("GremlinExecutorInit.groovy")), Collections.emptyMap())
                .create();

        assertEquals(2, gremlinExecutor.eval("add(1,1)").get());

        gremlinExecutor.close();
    }

    @Test
    public void shouldInitializeWithScriptAndMakeGlobalBinding() throws Exception {
        final GremlinExecutor gremlinExecutor = GremlinExecutor.build()
                .addEngineSettings("gremlin-groovy", Collections.emptyList(), Collections.emptyList(),
                        Arrays.asList(PATHS.get("GremlinExecutorInit.groovy")), Collections.emptyMap())
                .create();

        assertEquals(2, gremlinExecutor.eval("add(1,1)").get());
        assertThat(gremlinExecutor.getGlobalBindings().keySet(), not(contains("someSet")));
        assertThat(gremlinExecutor.getGlobalBindings().keySet(), contains("name"));

        assertEquals("stephen", gremlinExecutor.getGlobalBindings().get("name"));

        gremlinExecutor.close();
    }

    @Test
    public void shouldContinueToEvalScriptsEvenWithTimedInterrupt() throws Exception {
        final Map<String, List<Object>> compilerCustomizerConfig = new HashMap<>();
        final List<Object> args = new ArrayList<>();
        args.add(250);
        compilerCustomizerConfig.put(TimedInterruptCustomizerProvider.class.getName(), args);

        final Map<String, Object> config = new HashMap<>();
        config.put("compilerCustomizerProviders", compilerCustomizerConfig);

        final GremlinExecutor gremlinExecutor = GremlinExecutor.build()
                .addEngineSettings("gremlin-groovy", Collections.emptyList(), Collections.emptyList(),
                        Arrays.asList(PATHS.get("GremlinExecutorInit.groovy")), config)
                .create();

        for (int ix = 0; ix < 5; ix++) {
            try {
                // this script takes significantly longer than the interruptionTimeout
                gremlinExecutor
                        .eval("s = System.currentTimeMillis();\nwhile((System.currentTimeMillis() - s) < 10000) {}")
                        .get();
                fail("This should have timed out");
            } catch (Exception se) {
                assertEquals(TimedInterruptTimeoutException.class, se.getCause().getClass());
            }

            // this script takes significantly less than the interruptionTimeout
            assertEquals("test", gremlinExecutor
                    .eval("s = System.currentTimeMillis();\nwhile((System.currentTimeMillis() - s) < 20) {};'test'")
                    .get());
        }

        gremlinExecutor.close();
    }

    @Test
    public void shouldInterruptWhile() throws Exception {
        final Map<String, List<Object>> compilerCustomizerConfig = new HashMap<>();
        compilerCustomizerConfig.put(ThreadInterruptCustomizerProvider.class.getName(), new ArrayList<>());

        final Map<String, Object> config = new HashMap<>();
        config.put("compilerCustomizerProviders", compilerCustomizerConfig);

        final GremlinExecutor gremlinExecutor = GremlinExecutor.build()
                .addEngineSettings("gremlin-groovy", Collections.emptyList(), Collections.emptyList(),
                        Arrays.asList(PATHS.get("GremlinExecutorInit.groovy")), config)
                .create();
        final AtomicBoolean asserted = new AtomicBoolean(false);

        final Thread t = new Thread(() -> {
            try {
                gremlinExecutor
                        .eval("s = System.currentTimeMillis();\nwhile((System.currentTimeMillis() - s) < 10000) {}")
                        .get();
            } catch (Exception se) {
                asserted.set(se instanceof InterruptedException);
            }
        });

        t.start();
        Thread.sleep(100);
        t.interrupt();
        while (t.isAlive()) {
        }

        assertTrue(asserted.get());
    }

    @Test
    public void shouldInitializeWithScriptAndWorkAfterReset() throws Exception {
        final GremlinExecutor gremlinExecutor = GremlinExecutor.build()
                .addEngineSettings("gremlin-groovy", Collections.emptyList(), Collections.emptyList(),
                        Arrays.asList(PATHS.get("GremlinExecutorInit.groovy")), Collections.emptyMap())
                .create();

        assertEquals(2, gremlinExecutor.eval("add(1,1)").get());

        gremlinExecutor.getScriptEngines().reset();

        assertEquals(2, gremlinExecutor.eval("add(1,1)").get());

        gremlinExecutor.close();
    }

    @Test
    public void shouldNotShutdownExecutorServicesSuppliedToGremlinExecutor() throws Exception {
        final ScheduledExecutorService service = Executors.newScheduledThreadPool(4, testingThreadFactory);
        final GremlinExecutor gremlinExecutor = GremlinExecutor.build().executorService(service)
                .scheduledExecutorService(service).create();

        gremlinExecutor.close();
        assertFalse(service.isShutdown());
        service.shutdown();
        service.awaitTermination(30000, TimeUnit.MILLISECONDS);
    }

    @Test
    public void shouldGetExecutorService() throws Exception {
        final ScheduledExecutorService service = Executors.newScheduledThreadPool(4, testingThreadFactory);
        final GremlinExecutor gremlinExecutor = GremlinExecutor.build().executorService(service)
                .scheduledExecutorService(service).create();

        assertSame(service, gremlinExecutor.getExecutorService());
        gremlinExecutor.close();
    }

    @Test
    public void shouldGetScheduledExecutorService() throws Exception {
        final ScheduledExecutorService service = Executors.newScheduledThreadPool(4, testingThreadFactory);
        final GremlinExecutor gremlinExecutor = GremlinExecutor.build().executorService(service)
                .scheduledExecutorService(service).create();

        assertSame(service, gremlinExecutor.getScheduledExecutorService());
        gremlinExecutor.close();
    }

    @Test
    public void shouldAllowVariableReuseAcrossThreads() throws Exception {
        final ExecutorService service = Executors.newFixedThreadPool(8, testingThreadFactory);
        final GremlinExecutor gremlinExecutor = GremlinExecutor.build().create();

        final AtomicBoolean failed = new AtomicBoolean(false);
        final int max = 512;
        final List<Pair<Integer, List<Integer>>> futures = Collections.synchronizedList(new ArrayList<>(max));
        IntStream.range(0, max).forEach(i -> {
            final int yValue = i * 2;
            final Bindings b = new SimpleBindings();
            b.put("x", i);
            b.put("y", yValue);
            final int zValue = i * -1;

            final String script = "z=" + zValue + ";[x,y,z]";
            try {
                service.submit(() -> {
                    try {
                        final List<Integer> result = (List<Integer>) gremlinExecutor.eval(script, b).get();
                        futures.add(Pair.with(i, result));
                    } catch (Exception ex) {
                        failed.set(true);
                    }
                });
            } catch (Exception ex) {
                throw new RuntimeException(ex);
            }
        });

        service.shutdown();
        assertThat(service.awaitTermination(60000, TimeUnit.MILLISECONDS), is(true));

        // likely a concurrency exception if it occurs - and if it does then we've messed up because that's what this
        // test is partially designed to protected against.
        assertThat(failed.get(), is(false));

        assertEquals(max, futures.size());
        futures.forEach(t -> {
            assertEquals(t.getValue0(), t.getValue1().get(0));
            assertEquals(t.getValue0() * 2, t.getValue1().get(1).intValue());
            assertEquals(t.getValue0() * -1, t.getValue1().get(2).intValue());
        });
    }

    @Test
    public void shouldAllowConcurrentModificationOfGlobals() throws Exception {
        // this test simulates a scenario that likely shouldn't happen - where globals are modified by multiple
        // threads.  globals are created in a synchronized fashion typically but it's possible that someone
        // could do something like this and this test validate that concurrency exceptions don't occur as a
        // result
        final ExecutorService service = Executors.newFixedThreadPool(8, testingThreadFactory);
        final Bindings globals = new SimpleBindings();
        globals.put("g", -1);
        final GremlinExecutor gremlinExecutor = GremlinExecutor.build().globalBindings(globals).create();

        final AtomicBoolean failed = new AtomicBoolean(false);
        final int max = 512;
        final List<Pair<Integer, List<Integer>>> futures = Collections.synchronizedList(new ArrayList<>(max));
        IntStream.range(0, max).forEach(i -> {
            final int yValue = i * 2;
            final Bindings b = new SimpleBindings();
            b.put("x", i);
            b.put("y", yValue);
            final int zValue = i * -1;

            final String script = "z=" + zValue + ";[x,y,z,g]";
            try {
                service.submit(() -> {
                    try {
                        // modify the global in a separate thread
                        gremlinExecutor.getGlobalBindings().put("g", i);
                        gremlinExecutor.getGlobalBindings().put(Integer.toString(i), i);
                        gremlinExecutor.getGlobalBindings().keySet().stream()
                                .filter(s -> i % 2 == 0 && !s.equals("g")).findFirst().ifPresent(globals::remove);
                        final List<Integer> result = (List<Integer>) gremlinExecutor.eval(script, b).get();
                        futures.add(Pair.with(i, result));
                    } catch (Exception ex) {
                        failed.set(true);
                    }
                });
            } catch (Exception ex) {
                throw new RuntimeException(ex);
            }
        });

        service.shutdown();
        assertThat(service.awaitTermination(60000, TimeUnit.MILLISECONDS), is(true));

        // likely a concurrency exception if it occurs - and if it does then we've messed up because that's what this
        // test is partially designed to protected against.
        assertThat(failed.get(), is(false));

        assertEquals(max, futures.size());
        futures.forEach(t -> {
            assertEquals(t.getValue0(), t.getValue1().get(0));
            assertEquals(t.getValue0() * 2, t.getValue1().get(1).intValue());
            assertEquals(t.getValue0() * -1, t.getValue1().get(2).intValue());
            assertThat(t.getValue1().get(3).intValue(), greaterThan(-1));
        });
    }
}