Merge pull request #1892 from apache/TINKERPOP-2816-3.5

TINKERPOP-2816 for 3.5
diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc
index 43cce0b..8bd6826 100644
--- a/CHANGELOG.asciidoc
+++ b/CHANGELOG.asciidoc
@@ -30,7 +30,9 @@
 * Fixed issue where the `GremlinGroovyScriptEngine` reused the same translator concurrently which lead to incorrect translations.
 * Fixed bug where tasks that haven't started running yet time out due to `evaluationTimeout` and never send a response back to the client.
 * Set the exact exception in `initializationFailure` on the Java driver instead of the root cause.
+* Improved error message for when `from()` and `to()` are unproductive for `addE()`.
 * Added `SparkIOUtil` utility to load graph into Spark RDD.
+* Improved performance of `CloneVertexProgram` by bypassing the shuffle state of `SparkGraphComputer`.
 * Changed `JavaTranslator` exception handling so that an `IllegalArgumentException` is used for cases where the method exists but the signature can't be discerned given the arguments supplied.
 * Dockerized all test environment for .NET, JavaScript, Python, Go, and Python-based tests for Console, and added Docker as a build requirement.
 * Async operations in .NET can now be cancelled. This however does not cancel work that is already happening on the server.
@@ -39,13 +41,15 @@
 * Added user agent to web socket handshake in Gremlin.Net driver. Can be controlled by `EnableUserAgentOnConnect` in `ConnectionPoolSettings`. It is enabled by default.
 * Added user agent to web socket handshake in go driver. Can be controlled by a new `EnableUserAgentOnConnect` setting. It is enabled by default.
 * Added user agent to web socket handshake in python driver. Can be controlled by a new `enable_user_agent_on_connect` setting. It is enabled by default.
+* Added user agent to web socket handshake in javascript driver. Can be controlled by a new `enableUserAgentOnConnect` option. It is enabled by default.
 * Added logging in .NET.
 * Added `addDefaultXModule` to `GraphSONMapper` as a shortcut for including a version matched GraphSON extension module.
 * Modified `GraphSONRecordReader` and `GraphSONRecordWriter` to include the GraphSON extension module by default.
 * Bumped `jackson-databind` to 2.14.0 to fix security vulnerability.
-* Bumped to Groovy 2.5.19.
+* Bumped to Groovy 2.5.15.
 * Bumped to Netty 4.1.85.
 * Bumped `ivy` to 2.5.1 to fix security vulnerability
+* Removed default SSL handshake timeout. The SSL handshake timeout will instead be capped by setting `connectionSetupTimeoutMillis`.
 
 ==== Bugs
 
diff --git a/docs/src/dev/developer/for-committers.asciidoc b/docs/src/dev/developer/for-committers.asciidoc
index af2cbe0..627175d 100644
--- a/docs/src/dev/developer/for-committers.asciidoc
+++ b/docs/src/dev/developer/for-committers.asciidoc
@@ -162,7 +162,8 @@
 affect users or providers.  This label is important when organizing release notes.
 ** The "deprecation" label which is assigned to an issue that includes changes to deprecate a portion of the API.
 * The "affects/fix version(s)" fields should be appropriately set, where the "fix version" implies the version on
-which that particular issue will completed. This is a field usually only set by committers.
+which that particular issue will completed. This is a field usually only set by committers and should only be set
+when the issue is being closed with a completed disposition (e.g. "Done", "Fixed", etc.).
 * The "priority" field can be arbitrarily applied with one exception.  The "trivial" option should be reserved for
 tasks that are "easy" for a potential new contributor to jump into and do not have significant impact to urgently
 required improvements.
diff --git a/docs/src/reference/gremlin-variants.asciidoc b/docs/src/reference/gremlin-variants.asciidoc
index e445417..aec7f11 100644
--- a/docs/src/reference/gremlin-variants.asciidoc
+++ b/docs/src/reference/gremlin-variants.asciidoc
@@ -782,6 +782,34 @@
 g.V().hasLabel('person').has('age',gt(30)).order().by('age',desc).toList()
 ----
 
+[[gremlin-javascript-configuration]]
+=== Configuration
+The following table describes the various configuration options for the Gremlin-Javascript Driver. They
+can be passed in the constructor of a new `Client` or `DriverRemoteConnection` :
+
+[width="100%",cols="3,3,10,^2",options="header"]
+|=========================================================
+|Key |Type |Description |Default
+|url |String |The resource uri. |None
+|options |Object |The connection options. |{}
+|options.ca |Array |Trusted certificates. |undefined
+|options.cert |String/Array/Buffer |The certificate key. |undefined
+|options.mimeType |String |The mime type to use. |'application/vnd.gremlin-v3.0+json'
+|options.pfx |String/Buffer |The private key, certificate, and CA certs. |undefined
+|options.reader |GraphSONReader/GraphBinaryReader |The reader to use. |select reader according to mimeType
+|options.writer |GraphSONWriter |The writer to use. |select writer according to mimeType
+|options.rejectUnauthorized |Boolean |Determines whether to verify or not the server certificate. |undefined
+|options.traversalSource |String |The traversal source. |'g'
+|options.authenticator |Authenticator |The authentication handler to use. |undefined
+|options.processor |String |The name of the opProcessor to use, leave it undefined or set 'session' when session mode. |undefined
+|options.session |String |The sessionId of Client in session mode. undefined means session-less Client. |undefined
+|options.enableUserAgentOnConnect |Boolean |Determines if a user agent will be sent during connection handshake. |true
+|options.headers |Object |An associative array containing the additional header key/values for the initial request. |undefined
+|options.pingEnabled |Boolean |Setup ping interval. |true
+|options.pingInterval |Number |Ping request interval in ms if ping enabled. |60000
+|options.pongTimeout |Number |Timeout of pong response in ms after sending a ping. |30000
+|=========================================================
+
 [[gremlin-javascript-transactions]]
 === Transactions
 
diff --git a/gremlin-console/src/main/static/NOTICE b/gremlin-console/src/main/static/NOTICE
index 28800e7..c9f1534 100644
--- a/gremlin-console/src/main/static/NOTICE
+++ b/gremlin-console/src/main/static/NOTICE
@@ -18,7 +18,7 @@
 Copyright (c) 2008 Alexander Beider & Stephen P. Morse.
 
 ------------------------------------------------------------------------
-Apache Groovy 2.5.19 (AL ASF)
+Apache Groovy 2.5.15 (AL ASF)
 ------------------------------------------------------------------------
 This product includes/uses ANTLR (http://www.antlr2.org/)
 developed by Terence Parr 1989-2006
diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AddEdgeStep.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AddEdgeStep.java
index 1bf5d4a..4d3d864 100644
--- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AddEdgeStep.java
+++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AddEdgeStep.java
@@ -97,13 +97,30 @@
     @Override
     protected Edge map(final Traverser.Admin<S> traverser) {
         final String edgeLabel = this.parameters.get(traverser, T.label, () -> Edge.DEFAULT_LABEL).get(0);
-        final Object theTo = this.parameters.get(traverser, TO, traverser::get).get(0);
+
+        final Object theTo;
+        try {
+            theTo = this.parameters.get(traverser, TO, traverser::get).get(0);
+        } catch (IllegalArgumentException e) { // as thrown by TraversalUtil.apply()
+            throw new IllegalStateException(String.format(
+                    "addE(%s) failed because the to() traversal (which should give a Vertex) failed with: %s",
+                    edgeLabel, e.getMessage()));
+        }
+
         if (!(theTo instanceof Vertex))
             throw new IllegalStateException(String.format(
                     "addE(%s) could not find a Vertex for to() - encountered: %s", edgeLabel,
                     null == theTo ? "null" : theTo.getClass().getSimpleName()));
 
-        final Object theFrom = this.parameters.get(traverser, FROM, traverser::get).get(0);
+        final Object theFrom;
+        try {
+            theFrom  = this.parameters.get(traverser, FROM, traverser::get).get(0);
+        } catch (IllegalArgumentException e) { // as thrown by TraversalUtil.apply()
+            throw new IllegalStateException(String.format(
+                    "addE(%s) failed because the from() traversal (which should give a Vertex) failed with: %s",
+                    edgeLabel, e.getMessage()));
+        }
+
         if (!(theFrom instanceof Vertex))
             throw new IllegalStateException(String.format(
                     "addE(%s) could not find a Vertex for from() - encountered: %s", edgeLabel,
diff --git a/gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/Channelizer.java b/gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/Channelizer.java
index 67386f6..efe47c5 100644
--- a/gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/Channelizer.java
+++ b/gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/Channelizer.java
@@ -35,6 +35,7 @@
 import io.netty.handler.codec.http.websocketx.WebSocketVersion;
 import io.netty.handler.codec.http.websocketx.extensions.compression.WebSocketClientCompressionHandler;
 import io.netty.handler.ssl.SslContext;
+import io.netty.handler.ssl.SslHandler;
 import io.netty.handler.timeout.IdleStateHandler;
 
 import org.slf4j.Logger;
@@ -86,6 +87,7 @@
 
         protected static final String PIPELINE_GREMLIN_SASL_HANDLER = "gremlin-sasl-handler";
         protected static final String PIPELINE_GREMLIN_HANDLER = "gremlin-handler";
+        public static final String PIPELINE_SSL_HANDLER = "gremlin-ssl-handler";
 
         public boolean supportsSsl() {
             return cluster.connectionPoolSettings().enableSsl;
@@ -124,7 +126,12 @@
             }
 
             if (sslCtx.isPresent()) {
-                pipeline.addLast(sslCtx.get().newHandler(socketChannel.alloc(), connection.getUri().getHost(), connection.getUri().getPort()));
+                SslHandler sslHandler = sslCtx.get().newHandler(socketChannel.alloc(), connection.getUri().getHost(), connection.getUri().getPort());
+                // TINKERPOP-2814. Remove the SSL handshake timeout so that handshakes that take longer than 10000ms
+                // (Netty default) but less than connectionSetupTimeoutMillis can succeed. This means the SSL handshake
+                // will instead be capped by connectionSetupTimeoutMillis.
+                sslHandler.setHandshakeTimeoutMillis(0);
+                pipeline.addLast(PIPELINE_SSL_HANDLER, sslHandler);
             }
 
             configure(pipeline);
@@ -187,7 +194,7 @@
                     new WebSocketClientHandler.InterceptedWebSocketClientHandshaker13(
                             connection.getUri(), WebSocketVersion.V13, null, true,
                             httpHeaders, maxContentLength, true, false, -1,
-                            cluster.getHandshakeInterceptor()), cluster.getConnectionSetupTimeout());
+                            cluster.getHandshakeInterceptor()), cluster.getConnectionSetupTimeout(), supportsSsl());
 
             final int keepAliveInterval = toIntExact(TimeUnit.SECONDS.convert(
                     cluster.connectionPoolSettings().keepAliveInterval, TimeUnit.MILLISECONDS));
diff --git a/gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/Cluster.java b/gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/Cluster.java
index 89f8190..18e0876 100644
--- a/gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/Cluster.java
+++ b/gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/Cluster.java
@@ -993,9 +993,6 @@
         /**
          * Sets the duration of time in milliseconds provided for connection setup to complete which includes WebSocket
          * handshake and SSL handshake. Beyond this duration an exception would be thrown.
-         *
-         * Note that this value should be greater that SSL handshake timeout defined in
-         * {@link io.netty.handler.ssl.SslHandler} since WebSocket handshake include SSL handshake.
          */
         public Builder connectionSetupTimeoutMillis(final long connectionSetupTimeoutMillis) {
             this.connectionSetupTimeoutMillis = connectionSetupTimeoutMillis;
diff --git a/gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/Settings.java b/gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/Settings.java
index f016433..c6550d0 100644
--- a/gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/Settings.java
+++ b/gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/Settings.java
@@ -407,9 +407,6 @@
          * Duration of time in milliseconds provided for connection setup to complete which includes WebSocket
          * handshake and SSL handshake. Beyond this duration an exception would be thrown if the handshake is not
          * complete by then.
-         *
-         * Note that this value should be greater that SSL handshake timeout defined in
-         * {@link io.netty.handler.ssl.SslHandler} since WebSocket handshake include SSL handshake.
          */
         public long connectionSetupTimeoutMillis = Connection.CONNECTION_SETUP_TIMEOUT_MILLIS;
     }
diff --git a/gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/handler/WebSocketClientHandler.java b/gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/handler/WebSocketClientHandler.java
index 21aba24..82ae2fe 100644
--- a/gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/handler/WebSocketClientHandler.java
+++ b/gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/handler/WebSocketClientHandler.java
@@ -18,6 +18,7 @@
  */
 package org.apache.tinkerpop.gremlin.driver.handler;
 
+import io.netty.channel.Channel;
 import io.netty.channel.ChannelFuture;
 import io.netty.channel.ChannelHandlerContext;
 import io.netty.channel.ChannelPromise;
@@ -28,12 +29,18 @@
 import io.netty.handler.codec.http.websocketx.WebSocketClientProtocolHandler;
 import io.netty.handler.codec.http.websocketx.WebSocketClientHandshaker;
 import io.netty.handler.codec.http.websocketx.WebSocketVersion;
+import io.netty.handler.ssl.SslHandler;
+import io.netty.handler.ssl.SslHandshakeCompletionEvent;
 import io.netty.handler.timeout.IdleState;
 import io.netty.handler.timeout.IdleStateEvent;
+import io.netty.util.concurrent.Promise;
 
 import java.net.URI;
 import java.util.concurrent.TimeoutException;
 
+import javax.net.ssl.SSLHandshakeException;
+
+import org.apache.tinkerpop.gremlin.driver.Channelizer;
 import org.apache.tinkerpop.gremlin.driver.Cluster;
 import org.apache.tinkerpop.gremlin.driver.HandshakeInterceptor;
 import org.slf4j.Logger;
@@ -48,10 +55,13 @@
 
     private final long connectionSetupTimeoutMillis;
     private ChannelPromise handshakeFuture;
+    private boolean sslHandshakeCompleted;
+    private boolean useSsl;
 
-    public WebSocketClientHandler(final WebSocketClientHandshaker handshaker, final long timeoutMillis) {
+    public WebSocketClientHandler(final WebSocketClientHandshaker handshaker, final long timeoutMillis, final boolean useSsl) {
         super(handshaker, /*handleCloseFrames*/true, /*dropPongFrames*/true, timeoutMillis);
         this.connectionSetupTimeoutMillis = timeoutMillis;
+        this.useSsl = useSsl;
     }
 
     public ChannelFuture handshakeFuture() {
@@ -104,10 +114,21 @@
             }
         } else if (ClientHandshakeStateEvent.HANDSHAKE_TIMEOUT.equals(event)) {
             if (!handshakeFuture.isDone()) {
-                handshakeFuture.setFailure(
-                        new TimeoutException(String.format("handshake not completed in stipulated time=[%s]ms",
-                                connectionSetupTimeoutMillis)));
+                TimeoutException te = new TimeoutException(
+                        String.format((useSsl && !sslHandshakeCompleted) ?
+                                        "SSL handshake not completed in stipulated time=[%s]ms" :
+                                        "WebSocket handshake not completed in stipulated time=[%s]ms",
+                                connectionSetupTimeoutMillis));
+                handshakeFuture.setFailure(te);
+                logger.error(te.getMessage());
             }
+
+            if (useSsl && !sslHandshakeCompleted) {
+                SslHandler handler = ((SslHandler) ctx.pipeline().get(Channelizer.AbstractChannelizer.PIPELINE_SSL_HANDLER));
+                ((Promise<Channel>) handler.handshakeFuture()).tryFailure(new SSLHandshakeException("SSL handshake timed out."));
+            }
+        } else if (event instanceof SslHandshakeCompletionEvent) {
+            sslHandshakeCompleted = true;
         } else {
             super.userEventTriggered(ctx, event);
         }
diff --git a/gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/simple/WebSocketClient.java b/gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/simple/WebSocketClient.java
index 767f5a8..78f4268 100644
--- a/gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/simple/WebSocketClient.java
+++ b/gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/simple/WebSocketClient.java
@@ -69,7 +69,7 @@
 
         try {
             final WebSocketClientHandler wsHandler = new WebSocketClientHandler(WebSocketClientHandshakerFactory.newHandshaker(
-                    uri, WebSocketVersion.V13, null, true, EmptyHttpHeaders.INSTANCE, 65536), 10000);
+                    uri, WebSocketVersion.V13, null, true, EmptyHttpHeaders.INSTANCE, 65536), 10000, false);
             final MessageSerializer<GraphBinaryMapper> serializer = new GraphBinaryMessageSerializerV1();
             b.channel(NioSocketChannel.class)
                     .handler(new ChannelInitializer<SocketChannel>() {
diff --git a/gremlin-driver/src/test/java/org/apache/tinkerpop/gremlin/driver/NoOpWebSocketServerHandler.java b/gremlin-driver/src/test/java/org/apache/tinkerpop/gremlin/driver/NoOpWebSocketServerHandler.java
new file mode 100644
index 0000000..8c22c72
--- /dev/null
+++ b/gremlin-driver/src/test/java/org/apache/tinkerpop/gremlin/driver/NoOpWebSocketServerHandler.java
@@ -0,0 +1,42 @@
+/*
+ * 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.driver;
+
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.ChannelInboundHandlerAdapter;
+import io.netty.handler.codec.http.HttpRequest;
+import io.netty.util.ReferenceCountUtil;
+
+/**
+* Handler that will drop requests to the WebSocket path.
+*/
+public class NoOpWebSocketServerHandler extends ChannelInboundHandlerAdapter {
+    private String websocketPath;
+
+    public NoOpWebSocketServerHandler(String websocketPath) {
+        this.websocketPath = websocketPath;
+    }
+
+    @Override
+    public void channelRead(ChannelHandlerContext ctx, Object msg) {
+        if ((msg instanceof HttpRequest) && ((HttpRequest) msg).uri().endsWith(websocketPath)) {
+            ReferenceCountUtil.release(msg);
+        }
+    }
+}
diff --git a/gremlin-driver/src/test/java/org/apache/tinkerpop/gremlin/driver/TestHttpServerInitializer.java b/gremlin-driver/src/test/java/org/apache/tinkerpop/gremlin/driver/TestHttpServerInitializer.java
new file mode 100644
index 0000000..9985b70
--- /dev/null
+++ b/gremlin-driver/src/test/java/org/apache/tinkerpop/gremlin/driver/TestHttpServerInitializer.java
@@ -0,0 +1,40 @@
+/*
+ * 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.driver;
+
+import io.netty.channel.ChannelInitializer;
+import io.netty.channel.ChannelPipeline;
+import io.netty.channel.socket.SocketChannel;
+import io.netty.handler.codec.http.HttpObjectAggregator;
+import io.netty.handler.codec.http.HttpServerCodec;
+
+/**
+ * A base ChannelInitializer that setups the pipeline for HTTP handling. This class should be sub-classed by a handler
+ * that handles the actual data being received.
+ */
+public class TestHttpServerInitializer extends ChannelInitializer<SocketChannel> {
+    protected static final String WEBSOCKET_PATH = "/gremlin";
+
+    @Override
+    public void initChannel(SocketChannel ch) {
+        final ChannelPipeline pipeline = ch.pipeline();
+        pipeline.addLast(new HttpServerCodec());
+        pipeline.addLast(new HttpObjectAggregator(65536));
+    }
+}
diff --git a/gremlin-driver/src/test/java/org/apache/tinkerpop/gremlin/driver/TestWSNoOpInitializer.java b/gremlin-driver/src/test/java/org/apache/tinkerpop/gremlin/driver/TestWSNoOpInitializer.java
new file mode 100644
index 0000000..268144a
--- /dev/null
+++ b/gremlin-driver/src/test/java/org/apache/tinkerpop/gremlin/driver/TestWSNoOpInitializer.java
@@ -0,0 +1,33 @@
+/*
+ * 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.driver;
+
+import io.netty.channel.socket.SocketChannel;
+
+/**
+ * An initializer that adds a handler that will drop WebSocket frames.
+ */
+public class TestWSNoOpInitializer extends TestHttpServerInitializer {
+
+    @Override
+    public void initChannel(SocketChannel ch) {
+        super.initChannel(ch);
+        ch.pipeline().addLast(new NoOpWebSocketServerHandler(WEBSOCKET_PATH));
+    }
+}
diff --git a/gremlin-driver/src/test/java/org/apache/tinkerpop/gremlin/driver/TestWebSocketServerInitializer.java b/gremlin-driver/src/test/java/org/apache/tinkerpop/gremlin/driver/TestWebSocketServerInitializer.java
index 7857ae9..adfa723 100644
--- a/gremlin-driver/src/test/java/org/apache/tinkerpop/gremlin/driver/TestWebSocketServerInitializer.java
+++ b/gremlin-driver/src/test/java/org/apache/tinkerpop/gremlin/driver/TestWebSocketServerInitializer.java
@@ -18,11 +18,8 @@
  */
 package org.apache.tinkerpop.gremlin.driver;
 
-import io.netty.channel.ChannelInitializer;
 import io.netty.channel.ChannelPipeline;
 import io.netty.channel.socket.SocketChannel;
-import io.netty.handler.codec.http.HttpObjectAggregator;
-import io.netty.handler.codec.http.HttpServerCodec;
 import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
 import io.netty.handler.codec.http.websocketx.extensions.compression.WebSocketServerCompressionHandler;
 
@@ -30,14 +27,13 @@
  * A vanilla WebSocket server Initializer implementation using Netty. This initializer would configure the server for
  * WebSocket handshake and decoding incoming WebSocket frames.
  */
-public abstract class TestWebSocketServerInitializer extends ChannelInitializer<SocketChannel> {
-    private static final String WEBSOCKET_PATH = "/gremlin";
+public abstract class TestWebSocketServerInitializer extends TestHttpServerInitializer {
 
     @Override
     public void initChannel(SocketChannel ch) {
+        super.initChannel(ch);
+
         final ChannelPipeline pipeline = ch.pipeline();
-        pipeline.addLast(new HttpServerCodec());
-        pipeline.addLast(new HttpObjectAggregator(65536));
         pipeline.addLast(new WebSocketServerCompressionHandler());
         pipeline.addLast(new WebSocketServerProtocolHandler(WEBSOCKET_PATH, null, true));
         this.postInit(ch.pipeline());
diff --git a/gremlin-driver/src/test/java/org/apache/tinkerpop/gremlin/driver/WebSocketClientBehaviorIntegrateTest.java b/gremlin-driver/src/test/java/org/apache/tinkerpop/gremlin/driver/WebSocketClientBehaviorIntegrateTest.java
index 2ce10c2..7537721 100644
--- a/gremlin-driver/src/test/java/org/apache/tinkerpop/gremlin/driver/WebSocketClientBehaviorIntegrateTest.java
+++ b/gremlin-driver/src/test/java/org/apache/tinkerpop/gremlin/driver/WebSocketClientBehaviorIntegrateTest.java
@@ -26,6 +26,7 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.apache.log4j.Level;
+import org.apache.tinkerpop.gremlin.driver.exception.NoHostAvailableException;
 import org.apache.tinkerpop.gremlin.driver.ser.Serializers;
 import org.apache.tinkerpop.gremlin.structure.Vertex;
 import org.apache.tinkerpop.gremlin.util.Log4jRecordingAppender;
@@ -41,6 +42,7 @@
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
 
 public class WebSocketClientBehaviorIntegrateTest {
     @Rule
@@ -67,7 +69,12 @@
         rootLogger.addAppender(recordingAppender);
 
         server = new SimpleSocketServer();
-        server.start(new TestWSGremlinInitializer());
+        if (name.getMethodName().equals("shouldAttemptHandshakeForLongerThanDefaultNettySslHandshakeTimeout") ||
+                name.getMethodName().equals("shouldPrintCorrectErrorForRegularWebSocketHandshakeTimeout")) {
+            server.start(new TestWSNoOpInitializer());
+        } else {
+            server.start(new TestWSGremlinInitializer());
+        }
     }
 
     @After
@@ -309,4 +316,59 @@
                         .filter(str -> str.contains("Considering new connection on"))
                         .count());
     }
-}
\ No newline at end of file
+
+    /**
+     * (TINKERPOP-2814) Tests to make sure that the SSL handshake is now capped by connectionSetupTimeoutMillis and not
+     * the default Netty SSL handshake timeout of 10,000ms.
+     */
+    @Test
+    public void shouldAttemptHandshakeForLongerThanDefaultNettySslHandshakeTimeout() {
+        final Cluster cluster = Cluster.build("localhost").port(SimpleSocketServer.PORT)
+                .minConnectionPoolSize(1)
+                .maxConnectionPoolSize(1)
+                .connectionSetupTimeoutMillis(20000) // needs to be larger than 10000ms.
+                .enableSsl(true)
+                .create();
+
+        final Client.ClusteredClient client = cluster.connect();
+        final long start = System.currentTimeMillis();
+
+        Exception caught = null;
+        try {
+            client.submit("1");
+        } catch (Exception e) {
+            caught = e;
+        } finally {
+            // Test against 15000ms which should give a big enough buffer to avoid timing issues.
+            assertTrue(System.currentTimeMillis() - start > 15000);
+            assertTrue(caught != null);
+            assertTrue(caught instanceof NoHostAvailableException);
+            assertTrue(recordingAppender.getMessages().stream().anyMatch(str -> str.contains("SSL handshake not completed")));
+        }
+    }
+
+    /**
+     * Tests to make sure that the correct error message is logged when a non-SSL connection attempt times out.
+     */
+    @Test
+    public void shouldPrintCorrectErrorForRegularWebSocketHandshakeTimeout() {
+        final Cluster cluster = Cluster.build("localhost").port(SimpleSocketServer.PORT)
+                .minConnectionPoolSize(1)
+                .maxConnectionPoolSize(1)
+                .connectionSetupTimeoutMillis(100)
+                .create();
+
+        final Client.ClusteredClient client = cluster.connect();
+
+        Exception caught = null;
+        try {
+            client.submit("1");
+        } catch (Exception e) {
+            caught = e;
+        } finally {
+            assertTrue(caught != null);
+            assertTrue(caught instanceof NoHostAvailableException);
+            assertTrue(recordingAppender.getMessages().stream().anyMatch(str -> str.contains("WebSocket handshake not completed")));
+        }
+    }
+}
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 811b1fe..4f64f7e 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
@@ -39,8 +39,13 @@
    * @param {String} [options.traversalSource] The traversal source. Defaults to: 'g'.
    * @param {GraphSONWriter} [options.writer] The writer to use.
    * @param {Authenticator} [options.authenticator] The authentication handler to use.
+   * @param {Object} [options.headers] An associative array containing the additional header key/values for the initial request.
+   * @param {Boolean} [options.enableUserAgentOnConnect] Determines if a user agent will be sent during connection handshake. Defaults to: true
    * @param {String} [options.processor] The name of the opProcessor to use, leave it undefined or set 'session' when session mode.
    * @param {String} [options.session] The sessionId of Client in session mode. Defaults to null means session-less Client.
+   * @param {Boolean} [options.pingEnabled] Setup ping interval. Defaults to: true.
+   * @param {Number} [options.pingInterval] Ping request interval in ms if ping enabled. Defaults to: 60000.
+   * @param {Number} [options.pongTimeout] Timeout of pong response in ms after sending a ping. Defaults to: 30000.
    * @constructor
    */
   constructor(url, options = {}) {
diff --git a/gremlin-javascript/src/main/javascript/gremlin-javascript/lib/driver/connection.js b/gremlin-javascript/src/main/javascript/gremlin-javascript/lib/driver/connection.js
index cbbfb2e..acaeab4 100644
--- a/gremlin-javascript/src/main/javascript/gremlin-javascript/lib/driver/connection.js
+++ b/gremlin-javascript/src/main/javascript/gremlin-javascript/lib/driver/connection.js
@@ -64,6 +64,7 @@
    * @param {GraphSONWriter} [options.writer] The writer to use.
    * @param {Authenticator} [options.authenticator] The authentication handler to use.
    * @param {Object} [options.headers] An associative array containing the additional header key/values for the initial request.
+   * @param {Boolean} [options.enableUserAgentOnConnect] Determines if a user agent will be sent during connection handshake. Defaults to: true
    * @param {Boolean} [options.pingEnabled] Setup ping interval. Defaults to: true.
    * @param {Number} [options.pingInterval] Ping request interval in ms if ping enabled. Defaults to: 60000.
    * @param {Number} [options.pongTimeout] Timeout of pong response in ms after sending a ping. Defaults to: 30000.
@@ -98,6 +99,7 @@
     this.isOpen = false;
     this.traversalSource = options.traversalSource || 'g';
     this._authenticator = options.authenticator;
+    this._enableUserAgentOnConnect = options.enableUserAgentOnConnect !== false;
 
     this._pingEnabled = this.options.pingEnabled === false ? false : true;
     this._pingIntervalDelay = this.options.pingInterval || pingIntervalDelay;
@@ -123,9 +125,16 @@
     }
 
     this.emit('log', 'ws open');
+    let headers = this.options.headers;
+    if (this._enableUserAgentOnConnect) {
+      if (!headers) {
+        headers = [];
+      }
+      headers[utils.getUserAgentHeader()] = utils.getUserAgent();
+    }
 
     this._ws = new WebSocket(this.url, {
-      headers: this.options.headers,
+      headers: headers,
       ca: this.options.ca,
       cert: this.options.cert,
       pfx: this.options.pfx,
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 05a9345..869d621 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
@@ -48,6 +48,10 @@
    * @param {GraphSONWriter} [options.writer] The writer to use.
    * @param {Authenticator} [options.authenticator] The authentication handler to use.
    * @param {Object} [options.headers] An associative array containing the additional header key/values for the initial request.
+   * @param {Boolean} [options.enableUserAgentOnConnect] Determines if a user agent will be sent during connection handshake. Defaults to: true
+   * @param {Boolean} [options.pingEnabled] Setup ping interval. Defaults to: true.
+   * @param {Number} [options.pingInterval] Ping request interval in ms if ping enabled. Defaults to: 60000.
+   * @param {Number} [options.pongTimeout] Timeout of pong response in ms after sending a ping. Defaults to: 30000.
    * @constructor
    */
   constructor(url, options = {}) {
diff --git a/gremlin-javascript/src/main/javascript/gremlin-javascript/lib/utils.js b/gremlin-javascript/src/main/javascript/gremlin-javascript/lib/utils.js
index 6dd50ba..b7581d2 100644
--- a/gremlin-javascript/src/main/javascript/gremlin-javascript/lib/utils.js
+++ b/gremlin-javascript/src/main/javascript/gremlin-javascript/lib/utils.js
@@ -24,6 +24,8 @@
 'use strict';
 
 const crypto = require('crypto');
+const os = require('os');
+const gremlinVersion = require(__dirname + '/../package.json').version;
 
 exports.toLong = function toLong(value) {
   return new Long(value);
@@ -79,3 +81,25 @@
 }
 
 exports.ImmutableMap = ImmutableMap;
+
+function generateUserAgent() {
+  const applicationName = (process.env.npm_package_name || 'NotAvailable').replace('_', ' ');
+  const driverVersion = gremlinVersion.replace('_', ' ');
+  const runtimeVersion = process.version.replace(' ', '_');
+  const osName = os.platform().replace(' ', '_');
+  const osVersion = os.release().replace(' ', '_');
+  const cpuArch = process.arch.replace(' ', '_');
+  const userAgent = `${applicationName} Gremlin-Javascript.${driverVersion} ${runtimeVersion} ${osName}.${osVersion} ${cpuArch}`;
+
+  return userAgent;
+}
+
+exports.getUserAgentHeader = function getUserAgentHeader() {
+  return 'User-Agent';
+};
+
+const userAgent = generateUserAgent();
+
+exports.getUserAgent = function getUserAgent() {
+  return userAgent;
+};
diff --git a/gremlin-server/src/main/static/NOTICE b/gremlin-server/src/main/static/NOTICE
index 21ce164..7da4672 100644
--- a/gremlin-server/src/main/static/NOTICE
+++ b/gremlin-server/src/main/static/NOTICE
@@ -5,7 +5,7 @@
 The Apache Software Foundation (http://www.apache.org/).
 
 ------------------------------------------------------------------------
-Apache Groovy 2.5.19 (AL ASF)
+Apache Groovy 2.5.15 (AL ASF)
 ------------------------------------------------------------------------
 This product includes/uses ANTLR (http://www.antlr2.org/)
 developed by Terence Parr 1989-2006
diff --git a/gremlin-tools/gremlin-benchmark/pom.xml b/gremlin-tools/gremlin-benchmark/pom.xml
index 2a05e6a..46a43dd 100644
--- a/gremlin-tools/gremlin-benchmark/pom.xml
+++ b/gremlin-tools/gremlin-benchmark/pom.xml
@@ -27,7 +27,7 @@
     <artifactId>gremlin-benchmark</artifactId>
     <name>Apache TinkerPop :: Gremlin Benchmark</name>
     <properties>
-        <jmh.version>1.21</jmh.version>
+        <jmh.version>1.36</jmh.version>
         <!-- Skip benchmarks by default because they are time consuming. -->
         <skipBenchmarks>true</skipBenchmarks>
         <skipTests>${skipBenchmarks}</skipTests>
diff --git a/pom.xml b/pom.xml
index 27767f5..87b8700 100644
--- a/pom.xml
+++ b/pom.xml
@@ -158,7 +158,10 @@
         <commons.lang3.version>3.11</commons.lang3.version>
         <commons.text.version>1.10.0</commons.text.version>
         <exp4j.version>0.4.8</exp4j.version>
-        <groovy.version>2.5.19</groovy.version>
+        <!-- performance after 2.5.15 is similar to the poor performance of 3.x and 4.x - we can't upgrade past this
+             version without a accepting a major performance hit. details related to this issue along with links
+             to attempts to solve the problem with the Groovy community can be found on: TINKERPOP-2373 -->
+        <groovy.version>2.5.15</groovy.version>
         <hadoop.version>2.7.7</hadoop.version>
         <hamcrest.version>2.2</hamcrest.version>
         <java.tuples.version>1.2</java.tuples.version>
diff --git a/spark-gremlin/src/main/java/org/apache/tinkerpop/gremlin/spark/process/computer/SparkGraphComputer.java b/spark-gremlin/src/main/java/org/apache/tinkerpop/gremlin/spark/process/computer/SparkGraphComputer.java
index 9b7a012..62171ae 100644
--- a/spark-gremlin/src/main/java/org/apache/tinkerpop/gremlin/spark/process/computer/SparkGraphComputer.java
+++ b/spark-gremlin/src/main/java/org/apache/tinkerpop/gremlin/spark/process/computer/SparkGraphComputer.java
@@ -51,6 +51,7 @@
 import org.apache.tinkerpop.gremlin.process.computer.MapReduce;
 import org.apache.tinkerpop.gremlin.process.computer.Memory;
 import org.apache.tinkerpop.gremlin.process.computer.VertexProgram;
+import org.apache.tinkerpop.gremlin.process.computer.clone.CloneVertexProgram;
 import org.apache.tinkerpop.gremlin.process.computer.util.DefaultComputerResult;
 import org.apache.tinkerpop.gremlin.process.computer.util.MapMemory;
 import org.apache.tinkerpop.gremlin.process.traversal.TraversalStrategies;
@@ -59,6 +60,7 @@
 import org.apache.tinkerpop.gremlin.spark.process.computer.traversal.strategy.SparkVertexProgramInterceptor;
 import org.apache.tinkerpop.gremlin.spark.process.computer.traversal.strategy.optimization.SparkInterceptorStrategy;
 import org.apache.tinkerpop.gremlin.spark.process.computer.traversal.strategy.optimization.SparkSingleIterationStrategy;
+import org.apache.tinkerpop.gremlin.spark.process.computer.traversal.strategy.optimization.interceptor.SparkCloneVertexProgramInterceptor;
 import org.apache.tinkerpop.gremlin.spark.structure.Spark;
 import org.apache.tinkerpop.gremlin.spark.structure.io.InputFormatRDD;
 import org.apache.tinkerpop.gremlin.spark.structure.io.InputOutputHelper;
@@ -369,6 +371,12 @@
                 ////////////////////////////////
                 if (null != this.vertexProgram) {
                     memory = new SparkMemory(this.vertexProgram, this.mapReducers, sparkContext);
+                    // build a shortcut (which reduces the total Spark stages from 3 to 2) for CloneVertexProgram since it does nothing
+                    // and this improves the overall performance a lot
+                    if (this.vertexProgram.getClass().equals(CloneVertexProgram.class) &&
+                        !graphComputerConfiguration.containsKey(Constants.GREMLIN_HADOOP_VERTEX_PROGRAM_INTERCEPTOR)) {
+                        graphComputerConfiguration.setProperty(Constants.GREMLIN_HADOOP_VERTEX_PROGRAM_INTERCEPTOR, SparkCloneVertexProgramInterceptor.class.getName());
+                    }
                     /////////////////
                     // if there is a registered VertexProgramInterceptor, use it to bypass the GraphComputer semantics
                     if (graphComputerConfiguration.containsKey(Constants.GREMLIN_HADOOP_VERTEX_PROGRAM_INTERCEPTOR)) {
diff --git a/spark-gremlin/src/main/java/org/apache/tinkerpop/gremlin/spark/process/computer/traversal/strategy/optimization/interceptor/SparkCloneVertexProgramInterceptor.java b/spark-gremlin/src/main/java/org/apache/tinkerpop/gremlin/spark/process/computer/traversal/strategy/optimization/interceptor/SparkCloneVertexProgramInterceptor.java
new file mode 100644
index 0000000..8144032
--- /dev/null
+++ b/spark-gremlin/src/main/java/org/apache/tinkerpop/gremlin/spark/process/computer/traversal/strategy/optimization/interceptor/SparkCloneVertexProgramInterceptor.java
@@ -0,0 +1,34 @@
+/*
+ * 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.spark.process.computer.traversal.strategy.optimization.interceptor;
+
+import org.apache.spark.api.java.JavaPairRDD;
+import org.apache.tinkerpop.gremlin.hadoop.structure.io.VertexWritable;
+import org.apache.tinkerpop.gremlin.process.computer.VertexProgram;
+import org.apache.tinkerpop.gremlin.spark.process.computer.SparkMemory;
+import org.apache.tinkerpop.gremlin.spark.process.computer.traversal.strategy.SparkVertexProgramInterceptor;
+
+public class SparkCloneVertexProgramInterceptor implements SparkVertexProgramInterceptor<VertexProgram> {
+    @Override
+    public JavaPairRDD<Object, VertexWritable> apply(VertexProgram vertexProgram, JavaPairRDD<Object, VertexWritable> graph, SparkMemory memory) {
+        // bypass the VertexProgram since CloneVertexProgram does nothing in its execute method
+        return graph;
+    }
+}
diff --git a/spark-gremlin/src/test/java/org/apache/tinkerpop/gremlin/spark/process/computer/traversal/strategy/optimization/interceptor/SparkCloneVertexProgramInterceptorTest.java b/spark-gremlin/src/test/java/org/apache/tinkerpop/gremlin/spark/process/computer/traversal/strategy/optimization/interceptor/SparkCloneVertexProgramInterceptorTest.java
new file mode 100644
index 0000000..cd57d42
--- /dev/null
+++ b/spark-gremlin/src/test/java/org/apache/tinkerpop/gremlin/spark/process/computer/traversal/strategy/optimization/interceptor/SparkCloneVertexProgramInterceptorTest.java
@@ -0,0 +1,97 @@
+/*
+ * 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.spark.process.computer.traversal.strategy.optimization.interceptor;
+
+import org.apache.commons.configuration2.Configuration;
+import org.apache.hadoop.mapreduce.lib.output.NullOutputFormat;
+import org.apache.tinkerpop.gremlin.TestHelper;
+import org.apache.tinkerpop.gremlin.hadoop.Constants;
+import org.apache.tinkerpop.gremlin.hadoop.structure.io.gryo.GryoInputFormat;
+import org.apache.tinkerpop.gremlin.hadoop.structure.io.gryo.GryoOutputFormat;
+import org.apache.tinkerpop.gremlin.process.computer.clone.CloneVertexProgram;
+import org.apache.tinkerpop.gremlin.spark.AbstractSparkTest;
+import org.apache.tinkerpop.gremlin.spark.process.computer.SparkGraphComputer;
+import org.apache.tinkerpop.gremlin.spark.structure.io.gryo.GryoSerializerIntegrateTest;
+import org.apache.tinkerpop.gremlin.structure.Graph;
+import org.apache.tinkerpop.gremlin.structure.io.IoCore;
+import org.apache.tinkerpop.gremlin.structure.util.GraphFactory;
+import org.apache.tinkerpop.gremlin.tinkergraph.structure.TinkerGraph;
+import org.junit.Test;
+
+import java.util.UUID;
+
+import static org.junit.Assert.*;
+
+/**
+ * This class verify the default exporting behavior which requires the CloneVertexProgram's execute() is empty,
+ * then the SparkCloneVertexProgramInterceptor will bypass the VertexProgram.
+ */
+public class SparkCloneVertexProgramInterceptorTest extends AbstractSparkTest {
+    @Test
+    public void shouldExportCorrectGraph() throws Exception {
+        // Build the random graph
+        final TinkerGraph randomGraph = TinkerGraph.open();
+        final int totalVertices = 200000;
+        TestHelper.createRandomGraph(randomGraph, totalVertices, 100);
+        final String inputLocation = TestHelper.makeTestDataFile(GryoSerializerIntegrateTest.class,
+                UUID.randomUUID().toString(),
+                "random-graph.kryo");
+        randomGraph.io(IoCore.gryo()).writeGraph(inputLocation);
+        randomGraph.clear();
+        randomGraph.close();
+
+        // Serialize the graph to disk by CloneVertexProgram + SparkGraphComputer
+        final String outputLocation = TestHelper.makeTestDataDirectory(GryoSerializerIntegrateTest.class, UUID.randomUUID().toString());
+        Configuration configuration = getBaseConfiguration();
+        configuration.clearProperty(Constants.SPARK_SERIALIZER); // ensure proper default to GryoSerializer
+        configuration.setProperty(Constants.GREMLIN_HADOOP_INPUT_LOCATION, inputLocation);
+        configuration.setProperty(Constants.GREMLIN_HADOOP_GRAPH_READER, GryoInputFormat.class.getCanonicalName());
+        configuration.setProperty(Constants.GREMLIN_HADOOP_GRAPH_WRITER, GryoOutputFormat.class.getCanonicalName());
+        configuration.setProperty(Constants.GREMLIN_HADOOP_OUTPUT_LOCATION, outputLocation);
+        configuration.setProperty(Constants.GREMLIN_SPARK_PERSIST_CONTEXT, false);
+        Graph graph = GraphFactory.open(configuration);
+        graph.compute(SparkGraphComputer.class).program(CloneVertexProgram.build().create()).submit().get();
+
+        // Read the total Vertex/Edge count for golden reference through the original graph
+        configuration = getBaseConfiguration();
+        configuration.clearProperty(Constants.SPARK_SERIALIZER); // ensure proper default to GryoSerializer
+        configuration.setProperty(Constants.GREMLIN_HADOOP_INPUT_LOCATION, inputLocation);
+        configuration.setProperty(Constants.GREMLIN_HADOOP_GRAPH_READER, GryoInputFormat.class.getCanonicalName());
+        configuration.setProperty(Constants.GREMLIN_HADOOP_GRAPH_WRITER, NullOutputFormat.class.getCanonicalName());
+        configuration.setProperty(Constants.GREMLIN_SPARK_PERSIST_CONTEXT, false);
+        graph = GraphFactory.open(configuration);
+        long totalVRef = graph.traversal().withComputer(SparkGraphComputer.class).V().count().next().longValue();
+        long totalERef = graph.traversal().withComputer(SparkGraphComputer.class).E().count().next().longValue();
+
+        assertEquals(totalVRef, totalVertices);
+        // Read the total Vertex/Edge count from the exported graph
+        configuration = getBaseConfiguration();
+        configuration.setProperty(Constants.GREMLIN_HADOOP_INPUT_LOCATION, outputLocation);
+        configuration.setProperty(Constants.GREMLIN_HADOOP_GRAPH_READER, GryoInputFormat.class.getCanonicalName());
+        configuration.setProperty(Constants.GREMLIN_HADOOP_GRAPH_WRITER, NullOutputFormat.class.getCanonicalName());
+        configuration.setProperty(Constants.GREMLIN_SPARK_GRAPH_STORAGE_LEVEL, "MEMORY_ONLY"); // this should be ignored as you can't change the persistence level once created
+        configuration.setProperty(Constants.GREMLIN_SPARK_PERSIST_STORAGE_LEVEL, "MEMORY_AND_DISK");
+        configuration.setProperty(Constants.GREMLIN_SPARK_PERSIST_CONTEXT, true);
+        graph = GraphFactory.open(configuration);
+        long totalV = graph.traversal().withComputer(SparkGraphComputer.class).V().count().next().longValue();
+        long totalE = graph.traversal().withComputer(SparkGraphComputer.class).E().count().next().longValue();
+        assertEquals(totalV, totalVRef);
+        assertEquals(totalE, totalERef);
+    }
+}