You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@tinkerpop.apache.org by sp...@apache.org on 2021/06/24 20:54:29 UTC

[tinkerpop] 01/01: TINKERPOP-2557 Added support for g.tx() in gremlin-javascript

This is an automated email from the ASF dual-hosted git repository.

spmallette pushed a commit to branch TINKERPOP-2557
in repository https://gitbox.apache.org/repos/asf/tinkerpop.git

commit ef9bf6fb4815ebb4bf4e665f4605beef6e68414e
Author: Stephen Mallette <st...@amazon.com>
AuthorDate: Wed Jun 23 18:13:01 2021 -0400

    TINKERPOP-2557 Added support for g.tx() in gremlin-javascript
    
    Updated documentation. Modified the Gremlin Server setup within maven to react to -DincludeNeo4j so that it is possible to test transactions in language variants. Modified docker/gremlin-server.sh to dynamically include neo4j so that it matches the test server options when doing -DincludeNeo4j with maven.
---
 CHANGELOG.asciidoc                                 |   1 +
 docker/gremlin-server.sh                           |   7 +
 docker/gremlin-server/docker-entrypoint.sh         |   3 +
 .../gremlin-server/gremlin-server-integration.yaml |   3 +-
 docs/src/reference/gremlin-variants.asciidoc       |  39 +++++-
 docs/src/reference/the-traversal.asciidoc          |   6 +-
 .../gremlin/process/traversal/Bytecode.java        |   2 +-
 gremlin-javascript/pom.xml                         | 141 +++++++++++++++++++++
 .../gremlin-javascript/lib/driver/client.js        |  16 ++-
 .../lib/driver/driver-remote-connection.js         |  34 ++++-
 .../lib/driver/remote-connection.js                |  48 ++++++-
 .../lib/process/anonymous-traversal.js             |   2 +-
 .../gremlin-javascript/lib/process/bytecode.js     |  23 ++++
 .../lib/process/graph-traversal.js                 |  27 +++-
 .../gremlin-javascript/lib/process/transaction.js  |  86 +++++++++++++
 .../lib/process/traversal-strategy.js              |  10 ++
 .../gremlin-javascript/package-lock.json           |  10 +-
 .../test/integration/session-client-tests.js       |   2 +-
 .../test/integration/traversal-test.js             |  82 +++++++++++-
 .../test/unit/traversal-strategy-test.js           |  51 ++++++++
 .../gremlin-javascript/test/unit/traversal-test.js |  43 ++++++-
 .../src/test/scripts/generate-all.groovy           |   9 ++
 .../src/test/scripts/test-server-start.groovy      |  13 ++
 pom.xml                                            |   3 +-
 24 files changed, 631 insertions(+), 30 deletions(-)

diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc
index d80b694..724dc14 100644
--- a/CHANGELOG.asciidoc
+++ b/CHANGELOG.asciidoc
@@ -25,6 +25,7 @@ image::https://raw.githubusercontent.com/apache/tinkerpop/master/docs/static/ima
 
 This release also includes changes from <<release-3-4-12, 3.4.12>>.
 
+* Added support for `g.tx()` in Javascript.
 * Fixed bug in Javascript error message related to validating anonymous traversal spawns.
 * Changed close of Python and Javascript connections to no longer send a "close message" as the server no longer acknowledges it as of 3.5.0.
 * Removed sending of deprecated session close message from Gremlin.Net driver.
diff --git a/docker/gremlin-server.sh b/docker/gremlin-server.sh
index 9d91da7..8a9b675 100755
--- a/docker/gremlin-server.sh
+++ b/docker/gremlin-server.sh
@@ -18,6 +18,13 @@
 # under the License.
 #
 
+# ==================================================================
+# NOTE that Neo4j which is GPL licensed is dynamically installed and
+# configured for this test server as part of the
+# docker-entrypoint.sh which matches the functionality of the maven
+# build/test when run with -DincludeNeo4j.
+# ==================================================================
+
 # Build docker images first
 # Build gremlin-server:  mvn clean install -pl :gremlin-server -am && mvn install -Pdocker-images -pl :gremlin-server
 
diff --git a/docker/gremlin-server/docker-entrypoint.sh b/docker/gremlin-server/docker-entrypoint.sh
index 7031cec..d805092 100644
--- a/docker/gremlin-server/docker-entrypoint.sh
+++ b/docker/gremlin-server/docker-entrypoint.sh
@@ -38,6 +38,9 @@ cp *.yaml ${TINKERPOP_HOME}/conf/
 
 java -version
 
+# dynamically installs Neo4j libraries so that we can test variants with transactions.
+/opt/gremlin-server/bin/gremlin-server.sh install org.apache.tinkerpop neo4j-gremlin ${GREMLIN_SERVER_VERSION}
+
 /opt/gremlin-server/bin/gremlin-server.sh conf/gremlin-server-integration.yaml &
 
 /opt/gremlin-server/bin/gremlin-server.sh conf/gremlin-server-integration-secure.yaml &
diff --git a/docker/gremlin-server/gremlin-server-integration.yaml b/docker/gremlin-server/gremlin-server-integration.yaml
index 7f56673..53796d2 100644
--- a/docker/gremlin-server/gremlin-server-integration.yaml
+++ b/docker/gremlin-server/gremlin-server-integration.yaml
@@ -24,7 +24,8 @@ graphs: {
   modern: conf/tinkergraph-empty.properties,
   crew: conf/tinkergraph-empty.properties,
   grateful: conf/tinkergraph-empty.properties,
-  sink: conf/tinkergraph-empty.properties}
+  sink: conf/tinkergraph-empty.properties,
+  tx: conf/neo4j-empty.properties}
 scriptEngines: {
   gremlin-groovy: {
     plugins: { org.apache.tinkerpop.gremlin.server.jsr223.GremlinServerGremlinPlugin: {},
diff --git a/docs/src/reference/gremlin-variants.asciidoc b/docs/src/reference/gremlin-variants.asciidoc
index 955c56e..1aadc65 100644
--- a/docs/src/reference/gremlin-variants.asciidoc
+++ b/docs/src/reference/gremlin-variants.asciidoc
@@ -62,10 +62,10 @@ emulate its structure as best as possible given the constructs of the host langu
 variants ensures that the general Gremlin reference documentation is applicable to all variants and that users moving
 between development languages can easily adopt the Gremlin variant for that language.
 
-image::gremlin-variant-architecture.png[width=650,float=left]
+image::gremlin-variant-architecture.png[width=650]
 
 The following sections describe each language variant and driver that is officially TinkerPop a part of the project,
-provided more detailed information about usage, configuration and known limitations.
+providing more detailed information about usage, configuration and known limitations.
 
 anchor:connecting-via-remotegraph[]
 anchor:connecting-via-java[]
@@ -300,6 +300,12 @@ The following table describes the various configuration options for the Gremlin
 
 Please see the link:https://tinkerpop.apache.org/javadocs/x.y.z/core/org/apache/tinkerpop/gremlin/driver/Cluster.Builder.html[Cluster.Builder javadoc] to get more information on these settings.
 
+[[gremlin-java-transactions]]
+=== Transactions
+
+Transactions with Java are best described in <<transactions,The Traversal - Transactions>> section of this
+documentation as Java covers both embedded and remote use cases.
+
 [[gremlin-java-serialization]]
 === Serialization
 
@@ -1557,6 +1563,35 @@ const { order: { desc } } = gremlin.process;
 g.V().hasLabel('person').has('age',gt(30)).order().by('age',desc).toList()
 ----
 
+[[gremlin-javascript-transactions]]
+=== Transactions
+
+To get a full understanding of this section, it would be good to start by reading the <<transactions,Transactions>>
+section of this documentation, which discusses transactions in the general context of TinkerPop itself. This section
+builds on that content by demonstrating the transactional syntax for Javascript.
+
+[source,javascript]
+----
+const g = traversal().withRemote(new DriverRemoteConnection('ws://localhost:8182/gremlin'));
+const tx = g.tx(); // create a Transaction
+
+// spawn a new GraphTraversalSource binding all traversals established from it to tx
+const gtx = tx.begin();
+
+// execute traversals using gtx occur within the scope of the transaction held by tx. the
+// tx is closed after calls to commit or rollback and cannot be re-used. simply spawn a
+// new Transaction from g.tx() to create a new one as needed. the g context remains
+// accessible through all this as a sessionless connection.
+Promise.all([
+  gtx.addV("person").property("name", "jorge").iterate(),
+  gtx.addV("person").property("name", "josh").iterate()
+]).then(() => {
+  tx.commit();
+}).catch(() => {
+  tx.rollback();
+});
+----
+
 [[gremlin-javascript-lambda]]
 === The Lambda Solution
 
diff --git a/docs/src/reference/the-traversal.asciidoc b/docs/src/reference/the-traversal.asciidoc
index 199d2f7..39b1971 100644
--- a/docs/src/reference/the-traversal.asciidoc
+++ b/docs/src/reference/the-traversal.asciidoc
@@ -106,7 +106,7 @@ GraphTraversalSource gtx2 = tx2.begin();
 ----
 
 In remote cases, `GraphTraversalSource` instances spawned from `begin()` are safe to use in multiple threads though
-they on the server side they will be processed serially as they arrive. The default behavior of `close()` on a
+on the server side they will be processed serially as they arrive. The default behavior of `close()` on a
 `Transaction` for remote cases is to `commit()`, so the following re-write of the earlier example is also valid:
 
 [source,java]
@@ -123,6 +123,10 @@ try {
 }
 ----
 
+IMPORTANT: Transactions with non-JVM languages are always "remote". For specific transaction syntax in a particular
+language, please see the "Transactions" sub-section of your language of interest in the
+<<gremlin-drivers-variants,Gremlin Drivers and Variants>> section.
+
 In embedded cases, that initial recommended model for defining transactions holds, but users have more options here
 on deeper inspection. For embedded use cases (and perhaps even in configuration of a graph instance in Gremlin Server),
 the type of `Transaction` object that is returned from `g.tx()` is an important indicator as to the features of that
diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/Bytecode.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/Bytecode.java
index 56422c7..dc8eca3 100644
--- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/Bytecode.java
+++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/Bytecode.java
@@ -171,7 +171,7 @@ public final class Bytecode implements Cloneable, Serializable {
     public boolean equals(Object o) {
         if (this == o) return true;
         if (o == null || getClass() != o.getClass()) return false;
-        Bytecode bytecode = (Bytecode) o;
+        final Bytecode bytecode = (Bytecode) o;
         return Objects.equals(sourceInstructions, bytecode.sourceInstructions) &&
                 Objects.equals(stepInstructions, bytecode.stepInstructions);
     }
diff --git a/gremlin-javascript/pom.xml b/gremlin-javascript/pom.xml
index 5eb2f06..ea5055b 100644
--- a/gremlin-javascript/pom.xml
+++ b/gremlin-javascript/pom.xml
@@ -65,6 +65,16 @@ limitations under the License.
                         <version>${project.version}</version>
                     </dependency>
                     <dependency>
+                        <groupId>org.apache.tinkerpop</groupId>
+                        <artifactId>neo4j-gremlin</artifactId>
+                        <version>${project.version}</version>
+                    </dependency>
+                    <dependency>
+                        <groupId>commons-io</groupId>
+                        <artifactId>commons-io</artifactId>
+                        <version>${commons.io.version}</version>
+                    </dependency>
+                    <dependency>
                         <groupId>log4j</groupId>
                         <artifactId>log4j</artifactId>
                         <version>${log4j.version}</version>
@@ -243,6 +253,17 @@ file.write(file.getText("UTF-8").replaceFirst(/"version": "(.*)",/, "\"version\"
                     <workingDirectory>src/main/javascript/gremlin-javascript</workingDirectory>
                     <nodeVersion>${node.version}</nodeVersion>
                     <npmVersion>${npm.version}</npmVersion>
+
+                    <!--
+                    transaction testing is disabled unless the -DincludeNeo4j flag enables the include-neo4j
+                    maven profile which is a standard profile we use to add neo4j to testing explicitly - for
+                    npm we set this TEST_TRANSACTIONS environment variable that can be accessed in tests to
+                    determine if we skip transaction oriented tests or not. without neo4j we can't test tx()
+                    so this is disabled by default and enabled in the include-neo4j profile below
+                    -->
+                    <environmentVariables>
+                        <TEST_TRANSACTIONS>false</TEST_TRANSACTIONS>
+                    </environmentVariables>
                 </configuration>
             </plugin>
             <!--
@@ -280,6 +301,126 @@ file.write(file.getText("UTF-8").replaceFirst(/"version": "(.*)",/, "\"version\"
     </build>
     <profiles>
         <!--
+          This profile will include neo4j for purposes of transactional testing within Gremlin Server.
+          Tests that require neo4j specifically will be "ignored" if this profile is not turned on.
+        -->
+        <profile>
+            <id>include-neo4j</id>
+            <activation>
+                <activeByDefault>false</activeByDefault>
+                <property>
+                    <name>includeNeo4j</name>
+                </property>
+            </activation>
+            <build>
+                <plugins>
+                    <!-- with neo4j present we can enable transaction testing -->
+                    <plugin>
+                        <groupId>com.github.eirslett</groupId>
+                        <artifactId>frontend-maven-plugin</artifactId>
+                        <configuration>
+                            <environmentVariables combine.children="override">
+                                <TEST_TRANSACTIONS>true</TEST_TRANSACTIONS>
+                            </environmentVariables>
+                        </configuration>
+                    </plugin>
+                    <plugin>
+                        <groupId>org.codehaus.gmavenplus</groupId>
+                        <artifactId>gmavenplus-plugin</artifactId>
+                        <dependencies>
+                            <dependency>
+                                <groupId>org.neo4j</groupId>
+                                <artifactId>neo4j-tinkerpop-api-impl</artifactId>
+                                <version>0.9-3.4.0</version>
+                                <exclusions>
+                                    <exclusion>
+                                        <groupId>org.neo4j</groupId>
+                                        <artifactId>neo4j-kernel</artifactId>
+                                    </exclusion>
+                                    <exclusion>
+                                        <groupId>org.apache.commons</groupId>
+                                        <artifactId>commons-lang3</artifactId>
+                                    </exclusion>
+                                    <exclusion>
+                                        <groupId>org.apache.commons</groupId>
+                                        <artifactId>commons-text</artifactId>
+                                    </exclusion>
+                                    <exclusion>
+                                        <groupId>com.github.ben-manes.caffeine</groupId>
+                                        <artifactId>caffeine</artifactId>
+                                    </exclusion>
+                                    <exclusion>
+                                        <groupId>org.scala-lang</groupId>
+                                        <artifactId>scala-library</artifactId>
+                                    </exclusion>
+                                    <exclusion>
+                                        <groupId>org.scala-lang</groupId>
+                                        <artifactId>scala-reflect</artifactId>
+                                    </exclusion>
+                                    <exclusion>
+                                        <groupId>org.slf4j</groupId>
+                                        <artifactId>slf4j-api</artifactId>
+                                    </exclusion>
+                                    <exclusion>
+                                        <groupId>org.slf4j</groupId>
+                                        <artifactId>slf4j-nop</artifactId>
+                                    </exclusion>
+                                    <exclusion>
+                                        <groupId>org.apache.lucene</groupId>
+                                        <artifactId>lucene-core</artifactId>
+                                    </exclusion>
+                                    <exclusion>
+                                        <groupId>io.dropwizard.metrics</groupId>
+                                        <artifactId>metrics-core</artifactId>
+                                    </exclusion>
+                                    <exclusion>
+                                        <groupId>io.netty</groupId>
+                                        <artifactId>netty-all</artifactId>
+                                    </exclusion>
+                                    <exclusion>
+                                        <groupId>org.ow2.asm</groupId>
+                                        <artifactId>asm</artifactId>
+                                    </exclusion>
+                                </exclusions>
+                            </dependency>
+                            <dependency>
+                                <groupId>org.scala-lang</groupId>
+                                <artifactId>scala-library</artifactId>
+                                <version>2.11.8</version>
+                            </dependency>
+                            <dependency>
+                                <groupId>org.scala-lang</groupId>
+                                <artifactId>scala-reflect</artifactId>
+                                <version>2.11.8</version>
+                            </dependency>
+                            <dependency>
+                                <groupId>org.apache.lucene</groupId>
+                                <artifactId>lucene-core</artifactId>
+                                <version>5.5.0</version>
+                            </dependency>
+                            <dependency>
+                                <groupId>io.dropwizard.metrics</groupId>
+                                <artifactId>metrics-core</artifactId>
+                                <version>4.0.2</version>
+                            </dependency>
+                            <dependency>
+                                <groupId>org.neo4j</groupId>
+                                <artifactId>neo4j-kernel</artifactId>
+                                <version>3.4.11</version>
+                                <exclusions>
+                                    <exclusion>
+                                        <groupId>io.netty</groupId>
+                                        <artifactId>netty-all</artifactId>
+                                    </exclusion>
+                                </exclusions>
+                            </dependency>
+                        </dependencies>
+                    </plugin>
+                </plugins>
+            </build>
+        </profile>
+
+        <!--
         Provides a way to deploy the gremlin-javascript GLV to npm. This cannot be part of the standard maven execution
         because npm does not have a staging environment like sonatype for releases. As soon as the release is
         published it is public. In our release workflow, deploy occurs prior to vote on the release and we can't
diff --git a/gremlin-javascript/src/main/javascript/gremlin-javascript/lib/driver/client.js b/gremlin-javascript/src/main/javascript/gremlin-javascript/lib/driver/client.js
index 99fde77..fefb068 100644
--- a/gremlin-javascript/src/main/javascript/gremlin-javascript/lib/driver/client.js
+++ b/gremlin-javascript/src/main/javascript/gremlin-javascript/lib/driver/client.js
@@ -48,7 +48,7 @@ class Client {
     this._options = options;
     if (this._options.processor === 'session') {
       // compatibility with old 'session' processor setting
-      this._options.session = options.session || utils.getUuid()
+      this._options.session = options.session || utils.getUuid();
     }
     if (this._options.session) {
       // re-assign processor to 'session' when in session mode
@@ -93,16 +93,20 @@ class Client {
       aliases: { 'g': this._options.traversalSource || 'g' }
     }, requestOptions)
 
+    if (this._options.session && this._options.processor === 'session') {
+      args['session'] = this._options.session;
+    }
+
     if (message instanceof Bytecode) {
-      return this._connection.submit('traversal','bytecode', args, requestIdOverride);
+      if (this._options.session && this._options.processor === 'session') {
+        return this._connection.submit('session', 'bytecode', args, requestIdOverride);
+      } else {
+        return this._connection.submit('traversal', 'bytecode', args, requestIdOverride);
+      }
     } else if (typeof message === 'string') {
       args['bindings'] = bindings;
       args['language'] = 'gremlin-groovy';
       args['accept'] = this._connection.mimeType;
-
-      if (this._options.session && this._options.processor === 'session') {
-        args['session'] = this._options.session;
-      }
       return this._connection.submit(this._options.processor || '','eval', args, requestIdOverride);
     } else {
       throw new TypeError("message must be of type Bytecode or string");
diff --git a/gremlin-javascript/src/main/javascript/gremlin-javascript/lib/driver/driver-remote-connection.js b/gremlin-javascript/src/main/javascript/gremlin-javascript/lib/driver/driver-remote-connection.js
index f7d626d..18ffd15 100644
--- a/gremlin-javascript/src/main/javascript/gremlin-javascript/lib/driver/driver-remote-connection.js
+++ b/gremlin-javascript/src/main/javascript/gremlin-javascript/lib/driver/driver-remote-connection.js
@@ -25,7 +25,9 @@
 const rcModule = require('./remote-connection');
 const RemoteConnection = rcModule.RemoteConnection;
 const RemoteTraversal = rcModule.RemoteTraversal;
+const utils = require('../utils');
 const Client = require('./client');
+const Bytecode = require('../process/bytecode');
 const OptionsStrategy = require('../process/traversal-strategy').OptionsStrategy;
 
 /**
@@ -49,8 +51,8 @@ class DriverRemoteConnection extends RemoteConnection {
    * @param {Object} [options.headers] An associative array containing the additional header key/values for the initial request.
    * @constructor
    */
-  constructor(url, options) {
-    super(url);
+  constructor(url, options = {}) {
+    super(url, options);
     this._client = new Client(url, options);
   }
 
@@ -59,6 +61,7 @@ class DriverRemoteConnection extends RemoteConnection {
     return this._client.open();
   }
 
+  /** @override */
   get isOpen() {
     return this._client.isOpen;
   }
@@ -79,10 +82,37 @@ class DriverRemoteConnection extends RemoteConnection {
         }
       }
     }
+
     return this._client.submit(bytecode, null, requestOptions).then(result => new RemoteTraversal(result.toArray()));
   }
 
   /** @override */
+  createSession() {
+    if (this.isSessionBound)
+      throw new Error("Connection is already bound to a session - child sessions are not allowed");
+
+    // make sure a fresh session is used when starting a new transaction
+    const copiedOptions = Object.assign({}, this.options);
+    copiedOptions.session = utils.getUuid();
+    return new DriverRemoteConnection(this.url, copiedOptions);
+  }
+
+  /** @override */
+  get isSessionBound() {
+    return this.options.session;
+  }
+
+  /** @override */
+  commit() {
+    return this._client.submit(Bytecode.GraphOp.commit, null)
+  }
+
+  /** @override */
+  rollback() {
+    return this._client.submit(Bytecode.GraphOp.rollback, null)
+  }
+
+  /** @override */
   close() {
     return this._client.close();
   }
diff --git a/gremlin-javascript/src/main/javascript/gremlin-javascript/lib/driver/remote-connection.js b/gremlin-javascript/src/main/javascript/gremlin-javascript/lib/driver/remote-connection.js
index d53f320..435c72e 100644
--- a/gremlin-javascript/src/main/javascript/gremlin-javascript/lib/driver/remote-connection.js
+++ b/gremlin-javascript/src/main/javascript/gremlin-javascript/lib/driver/remote-connection.js
@@ -30,8 +30,14 @@ const TraversalStrategy = require('../process/traversal-strategy').TraversalStra
  * returning results.
  */
 class RemoteConnection {
-  constructor(url) {
+
+  /**
+   * @param {String} url The resource uri.
+   * @param {Object} [options] The connection options.
+   */
+  constructor(url, options = {}) {
     this.url = url;
+    this.options = options;
   }
 
   /**
@@ -51,6 +57,15 @@ class RemoteConnection {
   }
 
   /**
+   * Determines if the connection is already bound to a session. If so, this indicates that the
+   * <code>#createSession()</code> cannot be called so as to produce child sessions.
+   * @returns {boolean}
+   */
+  get isSessionBound() {
+    return false;
+  }
+
+  /**
    * Submits the <code>Bytecode</code> provided and returns a <code>RemoteTraversal</code>.
    * @abstract
    * @param {Bytecode} bytecode
@@ -61,7 +76,31 @@ class RemoteConnection {
   };
 
   /**
-   * Closes the connection, if its not already opened.
+   * Create a new <code>RemoteConnection</code> that is bound to a session using the configuration from this one.
+   * If the connection is already session bound then this function should throw an exception.
+   * @returns {RemoteConnection}
+   */
+  createSession() {
+    throw new Error('createSession() must be implemented');
+  }
+
+  /**
+   * Submits a <code>Bytecode.GraphOp.commit</code> to the server and closes the connection.
+   * @returns {Promise}
+   */
+  commit() {
+    throw new Error('commit() must be implemented');
+  }
+  /**
+   * Submits a <code>Bytecode.GraphOp.rollback</code> to the server and closes the connection.
+   * @returns {Promise}
+   */
+  rollback() {
+    throw new Error('rollback() must be implemented');
+  }
+
+  /**
+   * Closes the connection where open transactions will close according to the features of the graph provider.
    * @returns {Promise}
    */
   close() {
@@ -86,7 +125,10 @@ class RemoteStrategy extends TraversalStrategy {
    * @param {RemoteConnection} connection
    */
   constructor(connection) {
-    super();
+    // gave this a fqcn that has a local "js:" prefix since this strategy isn't sent as bytecode to the server.
+    // this is a sort of local-only strategy that actually executes client side. not sure if this prefix is the
+    // right way to name this or not, but it should have a name to identify it.
+    super("js:RemoteStrategy");
     this.connection = connection;
   }
 
diff --git a/gremlin-javascript/src/main/javascript/gremlin-javascript/lib/process/anonymous-traversal.js b/gremlin-javascript/src/main/javascript/gremlin-javascript/lib/process/anonymous-traversal.js
index 63e1e77..31757cc 100644
--- a/gremlin-javascript/src/main/javascript/gremlin-javascript/lib/process/anonymous-traversal.js
+++ b/gremlin-javascript/src/main/javascript/gremlin-javascript/lib/process/anonymous-traversal.js
@@ -52,7 +52,7 @@ class AnonymousTraversalSource {
   /**
    * Creates a {@link GraphTraversalSource} binding a {@link RemoteConnection} to a remote {@link Graph} instances as its
    * reference so that traversals spawned from it will execute over that reference.
-   * @param {GraphTraversalSource} remoteConnection
+   * @param {RemoteConnection} remoteConnection
    * @return {GraphTraversalSource}
    */
   withRemote(remoteConnection) {
diff --git a/gremlin-javascript/src/main/javascript/gremlin-javascript/lib/process/bytecode.js b/gremlin-javascript/src/main/javascript/gremlin-javascript/lib/process/bytecode.js
index 97d369e..85720dd 100644
--- a/gremlin-javascript/src/main/javascript/gremlin-javascript/lib/process/bytecode.js
+++ b/gremlin-javascript/src/main/javascript/gremlin-javascript/lib/process/bytecode.js
@@ -94,6 +94,29 @@ class Bytecode {
   toString() {
     return JSON.stringify([this.sourceInstructions, this.stepInstructions]);
   }
+
+  /**
+   * Adds a new source instructions
+   * @param {String} name
+   * @param {Array} values
+   * @returns {Bytecode}
+   */
+  static _createGraphOp(name, values) {
+    const bc = new Bytecode();
+    bc.addSource(name, values);
+    return bc;
+  }
+
+  /**
+   * Gets the <code>Bytecode</code> that is meant to be sent as "graph operations" to the server.
+   * @returns {{rollback: Bytecode, commit: Bytecode}}
+   */
+  static get GraphOp() {
+    return {
+      commit: Bytecode._createGraphOp("tx", ["commit"]),
+      rollback: Bytecode._createGraphOp("tx", ["rollback"])
+    };
+  }
 }
 
 module.exports = Bytecode;
diff --git a/gremlin-javascript/src/main/javascript/gremlin-javascript/lib/process/graph-traversal.js b/gremlin-javascript/src/main/javascript/gremlin-javascript/lib/process/graph-traversal.js
index 9421730..e45e0e2 100644
--- a/gremlin-javascript/src/main/javascript/gremlin-javascript/lib/process/graph-traversal.js
+++ b/gremlin-javascript/src/main/javascript/gremlin-javascript/lib/process/graph-traversal.js
@@ -23,12 +23,11 @@
 'use strict';
 
 const { Traversal } = require('./traversal');
+const { Transaction } = require('./transaction');
 const remote = require('../driver/remote-connection');
-const utils = require('../utils');
 const Bytecode = require('./bytecode');
 const { TraversalStrategies, VertexProgramStrategy, OptionsStrategy } = require('./traversal-strategy');
 
-
 /**
  * Represents the primary DSL of the Gremlin traversal machine.
  */
@@ -48,16 +47,36 @@ class GraphTraversalSource {
     this.bytecode = bytecode || new Bytecode();
     this.graphTraversalSourceClass = graphTraversalSourceClass || GraphTraversalSource;
     this.graphTraversalClass = graphTraversalClass || GraphTraversal;
+
+    // in order to keep the constructor unchanged within 3.5.x we can try to pop the RemoteConnection out of the
+    // TraversalStrategies. keeping this unchanged will allow user DSLs to not take a break.
+    // TODO: refactor this to be nicer in 3.6.0 when we can take a breaking change
+    const strat = traversalStrategies.strategies.find(ts => ts.fqcn === "js:RemoteStrategy");
+    this.remoteConnection = strat !== undefined ? strat.connection : undefined;
   }
 
   /**
-   * @param remoteConnection
+   * @param {RemoteConnection} remoteConnection
    * @returns {GraphTraversalSource}
    */
   withRemote(remoteConnection) {
     const traversalStrategy = new TraversalStrategies(this.traversalStrategies);
     traversalStrategy.addStrategy(new remote.RemoteStrategy(remoteConnection));
-    return new this.graphTraversalSourceClass(this.graph, traversalStrategy, new Bytecode(this.bytecode), this.graphTraversalSourceClass, this.graphTraversalClass);
+    return new this.graphTraversalSourceClass(this.graph, traversalStrategy, new Bytecode(this.bytecode),
+      this.graphTraversalSourceClass, this.graphTraversalClass);
+  }
+
+  /**
+   * Spawn a new <code>Transaction</code> object that can then start and stop a transaction.
+   * @returns {Transaction}
+   */
+  tx() {
+    // you can't do g.tx().begin().tx() - no child transactions
+    if (this.remoteConnection && this.remoteConnection.isSessionBound) {
+      throw new Error("This TraversalSource is already bound to a transaction - child transactions are not supported")
+    }
+
+    return new Transaction(this);
   }
 
   /**
diff --git a/gremlin-javascript/src/main/javascript/gremlin-javascript/lib/process/transaction.js b/gremlin-javascript/src/main/javascript/gremlin-javascript/lib/process/transaction.js
new file mode 100644
index 0000000..4110f9e
--- /dev/null
+++ b/gremlin-javascript/src/main/javascript/gremlin-javascript/lib/process/transaction.js
@@ -0,0 +1,86 @@
+/*
+ *  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.
+ */
+'use strict';
+
+const remote = require('../driver/remote-connection');
+const Bytecode = require('./bytecode');
+const { TraversalStrategies } = require('./traversal-strategy');
+
+/**
+ * A controller for a remote transaction that is constructed from <code>g.tx()</code>. Calling <code>begin()</code>
+ * on this object will produce a new <code>GraphTraversalSource</code> that is bound to a remote transaction over which
+ * multiple traversals may be executed in that context. Calling <code>commit()</code> or <code>rollback()</code> will
+ * then close the transaction and thus, the session. This feature only works with transaction enabled graphs.
+ */
+class Transaction {
+  constructor(g) {
+    this._g = g;
+    this._sessionBasedConnection = undefined;
+  }
+
+  /**
+   * Spawns a <code>GraphTraversalSource</code> that is bound to a remote session which enables a transaction.
+   * @returns {*}
+   */
+  begin() {
+    if (this._sessionBasedConnection) {
+      throw new Error("Transaction already started on this object");
+    }
+
+    this._sessionBasedConnection = this._g.remoteConnection.createSession();
+    const traversalStrategy = new TraversalStrategies();
+    traversalStrategy.addStrategy(new remote.RemoteStrategy(this._sessionBasedConnection));
+    return new this._g.graphTraversalSourceClass(this._g.graph, traversalStrategy, new Bytecode(this._g.bytecode),
+      this._g.graphTraversalSourceClass, this._g.graphTraversalClass);
+  }
+
+  /**
+   * @returns {Promise}
+   */
+  commit() {
+    return this._sessionBasedConnection.commit().then(() => this.close());
+  }
+
+  /**
+   * @returns {Promise}
+   */
+  rollback() {
+    return this._sessionBasedConnection.rollback().then(() => this.close());
+  }
+
+  /**
+   * Returns true if transaction is open.
+   * @returns {Boolean}
+   */
+  get isOpen() {
+    this._sessionBasedConnection.isOpen;
+  }
+
+  /**
+   * @returns {Promise}
+   */
+  close() {
+    if (this._sessionBasedConnection)
+      this._sessionBasedConnection.close();
+  }
+}
+
+module.exports = {
+  Transaction
+};
\ No newline at end of file
diff --git a/gremlin-javascript/src/main/javascript/gremlin-javascript/lib/process/traversal-strategy.js b/gremlin-javascript/src/main/javascript/gremlin-javascript/lib/process/traversal-strategy.js
index 37816bd..7539a26 100644
--- a/gremlin-javascript/src/main/javascript/gremlin-javascript/lib/process/traversal-strategy.js
+++ b/gremlin-javascript/src/main/javascript/gremlin-javascript/lib/process/traversal-strategy.js
@@ -45,6 +45,16 @@ class TraversalStrategies {
     this.strategies.push(strategy);
   }
 
+  /** @param {TraversalStrategy} strategy */
+  removeStrategy(strategy) {
+    const idx = this.strategies.findIndex(s => s.fqcn === strategy.fqcn);
+    if (idx !== -1) {
+      return this.strategies.splice(idx, 1)[0];
+    }
+
+    return undefined;
+  }
+
   /**
    * @param {Traversal} traversal
    * @returns {Promise}
diff --git a/gremlin-javascript/src/main/javascript/gremlin-javascript/package-lock.json b/gremlin-javascript/src/main/javascript/gremlin-javascript/package-lock.json
index fa9f9ab..35baa25 100644
--- a/gremlin-javascript/src/main/javascript/gremlin-javascript/package-lock.json
+++ b/gremlin-javascript/src/main/javascript/gremlin-javascript/package-lock.json
@@ -1666,7 +1666,7 @@
     "mocha": {
       "version": "5.2.0",
       "resolved": "https://registry.npmjs.org/mocha/-/mocha-5.2.0.tgz",
-      "integrity": "sha1-bYrlCPWRZ/lA8rWzxKYSrlDJCuY=",
+      "integrity": "sha512-2IUgKDhc3J7Uug+FxMXuqIyYzH7gJjXECKe/w43IGgQHTSj3InJi+yAA7T24L9bQMRKiUEHxEX37G5JpVUGLcQ==",
       "dev": true,
       "requires": {
         "browser-stdout": "1.3.1",
@@ -1685,13 +1685,13 @@
         "commander": {
           "version": "2.15.1",
           "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz",
-          "integrity": "sha1-30boZ9D8Kuxmo0ZitAapzK//Ww8=",
+          "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==",
           "dev": true
         },
         "debug": {
           "version": "3.1.0",
           "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
-          "integrity": "sha1-W7WgZyYotkFJVmuhaBnmFRjGcmE=",
+          "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
           "dev": true,
           "requires": {
             "ms": "2.0.0"
@@ -1700,7 +1700,7 @@
         "glob": {
           "version": "7.1.2",
           "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz",
-          "integrity": "sha1-wZyd+aAocC1nhhI4SmVSQExjbRU=",
+          "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==",
           "dev": true,
           "requires": {
             "fs.realpath": "^1.0.0",
@@ -1723,7 +1723,7 @@
         "supports-color": {
           "version": "5.4.0",
           "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz",
-          "integrity": "sha1-HGszdALCE3YF7+GfEP7DkPb6q1Q=",
+          "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==",
           "dev": true,
           "requires": {
             "has-flag": "^3.0.0"
diff --git a/gremlin-javascript/src/main/javascript/gremlin-javascript/test/integration/session-client-tests.js b/gremlin-javascript/src/main/javascript/gremlin-javascript/test/integration/session-client-tests.js
index 87190d0..59cd1c6 100644
--- a/gremlin-javascript/src/main/javascript/gremlin-javascript/test/integration/session-client-tests.js
+++ b/gremlin-javascript/src/main/javascript/gremlin-javascript/test/integration/session-client-tests.js
@@ -48,7 +48,7 @@ describe('Client', function () {
         });
     });
 
-    it('should use golbal cache in session', function () {
+    it('should use global cache in session', function () {
       return client.submit("x = [0, 1, 2, 3, 4, 5]")
         .then(function (result) {
           assert.ok(result);
diff --git a/gremlin-javascript/src/main/javascript/gremlin-javascript/test/integration/traversal-test.js b/gremlin-javascript/src/main/javascript/gremlin-javascript/test/integration/traversal-test.js
index 9cf62cb..bc19e8a 100644
--- a/gremlin-javascript/src/main/javascript/gremlin-javascript/test/integration/traversal-test.js
+++ b/gremlin-javascript/src/main/javascript/gremlin-javascript/test/integration/traversal-test.js
@@ -25,7 +25,6 @@
 const Mocha = require('mocha');
 const assert = require('assert');
 const { AssertionError } = require('assert');
-const expect = require('chai').expect;
 const DriverRemoteConnection = require('../../lib/driver/driver-remote-connection');
 const { Vertex } = require('../../lib/structure/graph');
 const { traversal } = require('../../lib/process/anonymous-traversal');
@@ -37,6 +36,7 @@ const helper = require('../helper');
 const __ = statics;
 
 let connection;
+let txConnection;
 
 class SocialTraversal extends GraphTraversal {
   constructor(graph, traversalStrategies, bytecode) {
@@ -211,4 +211,84 @@ describe('Traversal', function () {
       return g.V().repeat(__.both()).iterate().then(() => assert.fail("should have tanked"), (err) => assert.strictEqual(err.statusCode, 598));
     });
   });
+  describe('support remote transactions - commit', function() {
+    before(function () {
+      if (process.env.TEST_TRANSACTIONS !== "true") return this.skip();
+
+      txConnection = helper.getConnection('gtx');
+      return txConnection.open();
+    });
+    after(function () {
+      if (process.env.TEST_TRANSACTIONS === "true") {
+        // neo4j gets re-used and has to be cleaned up per test that uses it
+        const g = traversal().withRemote(txConnection);
+        return g.V().drop().iterate().then(() => {
+          return txConnection.close()
+        });
+      }
+    });
+    it('should commit a simple transaction', function () {
+      const g = traversal().withRemote(txConnection);
+      const tx = g.tx();
+      const gtx = tx.begin();
+      return Promise.all([
+        gtx.addV("person").property("name", "jorge").iterate(),
+        gtx.addV("person").property("name", "josh").iterate()
+      ]).then(() => {
+        return gtx.V().count().next();
+      }).then(function (r) {
+        // assert within the transaction....
+        assert.ok(r);
+        assert.strictEqual(r.value, 2);
+
+        // now commit changes to test outside of the transaction
+        return tx.commit();
+      }).then(() => {
+        return g.V().count().next();
+      }).then(function (r) {
+        assert.ok(r);
+        assert.strictEqual(r.value, 2);
+      });
+    });
+  });
+  describe('support remote transactions - rollback', function() {
+    before(function () {
+      if (process.env.TEST_TRANSACTIONS !== "true") return this.skip();
+
+      txConnection = helper.getConnection('gtx');
+      return txConnection.open();
+    });
+    after(function () {
+      if (process.env.TEST_TRANSACTIONS === "true") {
+        // neo4j gets re-used and has to be cleaned up per test that uses it
+        const g = traversal().withRemote(txConnection);
+        return g.V().drop().iterate().then(() => {
+          return txConnection.close()
+        });
+      }
+    });
+    it('should rollback a simple transaction', function() {
+      const g = traversal().withRemote(txConnection);
+      const tx = g.tx();
+      const gtx = tx.begin();
+      return Promise.all([
+        gtx.addV("person").property("name", "jorge").iterate(),
+        gtx.addV("person").property("name", "josh").iterate()
+      ]).then(() => {
+        return gtx.V().count().next();
+      }).then(function (r) {
+        // assert within the transaction....
+        assert.ok(r);
+        assert.strictEqual(r.value, 2);
+
+        // now rollback changes to test outside of the transaction
+        return tx.rollback();
+      }).then(() => {
+        return g.V().count().next();
+      }).then(function (r) {
+        assert.ok(r);
+        assert.strictEqual(r.value, 0);
+      });
+    });
+  });
 });
\ No newline at end of file
diff --git a/gremlin-javascript/src/main/javascript/gremlin-javascript/test/unit/traversal-strategy-test.js b/gremlin-javascript/src/main/javascript/gremlin-javascript/test/unit/traversal-strategy-test.js
new file mode 100644
index 0000000..fbfbcf1
--- /dev/null
+++ b/gremlin-javascript/src/main/javascript/gremlin-javascript/test/unit/traversal-strategy-test.js
@@ -0,0 +1,51 @@
+/*
+ *  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.
+ */
+
+'use strict';
+
+const assert = require('assert');
+const { TraversalStrategies, OptionsStrategy, ConnectiveStrategy } = require('../../lib/process/traversal-strategy');
+
+describe('TraversalStrategies', function () {
+
+  describe('#removeStrategy()', function () {
+    it('should remove strategy', function () {
+      const ts = new TraversalStrategies()
+      ts.addStrategy(new ConnectiveStrategy());
+      ts.addStrategy(new OptionsStrategy({x: 123}));
+      assert.strictEqual(ts.strategies.length, 2);
+
+      const c = new OptionsStrategy({x: 123});
+      const os = ts.removeStrategy(c);
+      assert.strictEqual(os.fqcn, c.fqcn);
+      assert.strictEqual(ts.strategies.length, 1);
+
+      ts.removeStrategy(new ConnectiveStrategy());
+      assert.strictEqual(ts.strategies.length, 0);
+    });
+
+    it('should not find anything to remove', function () {
+      const ts = new TraversalStrategies()
+      ts.addStrategy(new OptionsStrategy({x: 123}));
+      assert.strictEqual(ts.strategies.length, 1);
+      ts.removeStrategy(new ConnectiveStrategy());
+      assert.strictEqual(ts.strategies.length, 1);
+    });
+  });
+});
\ No newline at end of file
diff --git a/gremlin-javascript/src/main/javascript/gremlin-javascript/test/unit/traversal-test.js b/gremlin-javascript/src/main/javascript/gremlin-javascript/test/unit/traversal-test.js
index 273f2c4..b3a9892 100644
--- a/gremlin-javascript/src/main/javascript/gremlin-javascript/test/unit/traversal-test.js
+++ b/gremlin-javascript/src/main/javascript/gremlin-javascript/test/unit/traversal-test.js
@@ -32,6 +32,7 @@ const V = gt.statics.V;
 const P = t.P;
 const Bytecode = require('../../lib/process/bytecode');
 const TraversalStrategies = require('../../lib/process/traversal-strategy').TraversalStrategies;
+const RemoteConnection = require('../../lib/driver/remote-connection').RemoteConnection;
 
 describe('Traversal', function () {
 
@@ -245,4 +246,44 @@ describe('Traversal', function () {
       });
     })
   });
-});
\ No newline at end of file
+
+  describe('child transactions', function() {
+    it('should not support child transactions', function() {
+      const g = anon.traversal().withRemote(new MockRemoteConnection());
+      const tx = g.tx();
+      assert.throws(function() {
+        tx.begin().tx();
+      });
+    });
+  });
+
+  describe('tx#begin()', function() {
+    it("should not allow a transaction to begin more than once", function() {
+      const g = anon.traversal().withRemote(new MockRemoteConnection());
+      const tx = g.tx();
+      tx.begin();
+      assert.throws(function () {
+        tx.begin();
+      });
+    });
+  });
+});
+
+class MockRemoteConnection extends RemoteConnection {
+  constructor(bound = false) {
+    super('ws://localhost:9998/gremlin');
+    this._bound = bound;
+  }
+
+  get isSessionBound() {
+    return this._bound;
+  }
+
+  submit(bytecode) {
+    return Promise.resolve(undefined);
+  }
+
+  createSession() {
+    return new MockRemoteConnection(true);
+  }
+}
\ No newline at end of file
diff --git a/gremlin-server/src/test/scripts/generate-all.groovy b/gremlin-server/src/test/scripts/generate-all.groovy
index f49351b..464f673 100644
--- a/gremlin-server/src/test/scripts/generate-all.groovy
+++ b/gremlin-server/src/test/scripts/generate-all.groovy
@@ -67,3 +67,12 @@ globals << [gcrew : traversal().withEmbedded(crew).withStrategies(ReferenceEleme
 globals << [ggraph : traversal().withEmbedded(graph).withStrategies(ReferenceElementStrategy)]
 globals << [ggrateful : traversal().withEmbedded(grateful).withStrategies(ReferenceElementStrategy)]
 globals << [gsink : traversal().withEmbedded(sink).withStrategies(ReferenceElementStrategy)]
+
+// dynamically detect existence of gtx as it may or may not be present depending on the -DincludeNeo4j
+// and the configuration of the particular server instance. with docker/gremlin-server.sh the neo4j
+// "tx" configuration is already present and will therefore be enabled.
+def dynamicGtx = context.getBindings(javax.script.ScriptContext.GLOBAL_SCOPE)["tx"]
+if (dynamicGtx != null)
+    globals << [gtx : traversal().withEmbedded(dynamicGtx).withStrategies(ReferenceElementStrategy)]
+
+globals
diff --git a/gremlin-server/src/test/scripts/test-server-start.groovy b/gremlin-server/src/test/scripts/test-server-start.groovy
index ca59745..98e81e5 100644
--- a/gremlin-server/src/test/scripts/test-server-start.groovy
+++ b/gremlin-server/src/test/scripts/test-server-start.groovy
@@ -34,8 +34,20 @@ import org.apache.tinkerpop.gremlin.server.Settings
 ////////////////////////////////////////////////////////////////////////////////
 
 if (Boolean.parseBoolean(skipTests)) return
+def testTransactions = System.getProperty("includeNeo4j") != null
+
+if (testTransactions) {
+    // clean up prior neo4j instances
+    def tempNeo4jDir = new java.io.File("/tmp/neo4j")
+    if (tempNeo4jDir.exists()) {
+        log.info("Cleaning up prior Neo4j test instances for ${executionName}")
+        org.apache.commons.io.FileUtils.deleteDirectory(tempNeo4jDir)
+    }
+}
 
 log.info("Starting Gremlin Server instances for native testing of ${executionName}")
+log.info("Transactions validated (enabled with -DincludeNeo4j and only available on port 45940): " + testTransactions)
+
 def settings = Settings.read("${settingsFile}")
 settings.graphs.graph = gremlinServerDir + "/src/test/scripts/tinkergraph-empty.properties"
 settings.graphs.classic = gremlinServerDir + "/src/test/scripts/tinkergraph-empty.properties"
@@ -43,6 +55,7 @@ settings.graphs.modern = gremlinServerDir + "/src/test/scripts/tinkergraph-empty
 settings.graphs.crew = gremlinServerDir + "/src/test/scripts/tinkergraph-empty.properties"
 settings.graphs.grateful = gremlinServerDir + "/src/test/scripts/tinkergraph-empty.properties"
 settings.graphs.sink = gremlinServerDir + "/src/test/scripts/tinkergraph-empty.properties"
+if (testTransactions) settings.graphs.tx = gremlinServerDir + "/src/test/scripts/neo4j-empty.properties"
 settings.scriptEngines["gremlin-groovy"].plugins["org.apache.tinkerpop.gremlin.jsr223.ScriptFileGremlinPlugin"].files = [gremlinServerDir + "/src/test/scripts/generate-all.groovy"]
 settings.port = 45940
 
diff --git a/pom.xml b/pom.xml
index ea378c2..f589fb7 100644
--- a/pom.xml
+++ b/pom.xml
@@ -150,6 +150,7 @@ limitations under the License.
     <properties>
         <commons.configuration.version>2.7</commons.configuration.version>
         <commons.lang.version>2.6</commons.lang.version>
+        <commons.io.version>2.8.0</commons.io.version>
         <commons.lang3.version>3.11</commons.lang3.version>
         <commons.text.version>1.9</commons.text.version>
         <groovy.version>2.5.14</groovy.version>
@@ -826,7 +827,7 @@ limitations under the License.
             <dependency>
                 <groupId>commons-io</groupId>
                 <artifactId>commons-io</artifactId>
-                <version>2.8.0</version>
+                <version>${commons.io.version}</version>
             </dependency>
             <dependency>
                 <groupId>commons-codec</groupId>