You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@tinkerpop.apache.org by va...@apache.org on 2023/02/09 19:30:26 UTC

[tinkerpop] branch master updated: Add RequestOptions to Go GLV

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

valentyn pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/tinkerpop.git


The following commit(s) were added to refs/heads/master by this push:
     new c16da29f19 Add RequestOptions to Go GLV
     new 81fc436863 Merge pull request #1939 from Bit-Quill/cole/go-per-request-settings
c16da29f19 is described below

commit c16da29f190d071e9005917497c36e77da24918d
Author: Cole Greer <co...@bitquilltech.com>
AuthorDate: Wed Jan 4 17:50:37 2023 -0800

    Add RequestOptions to Go GLV
    
    Go was previously not sending per-request options correctly to the server.
    Go was passing request options inside the gremlin script bindings map instead of
    within the args portion of the request message.
    
    This commit resolves this issue by encapsulating all per-request settings as well as
    script bindings into a new RequestOptions struct which has an accompanying RequestOptionsBuilder.
    
    This can now be passed in through new Client.SubmitWithOptions() and DriverRemoteConnection.SubmitWithOptions() methods.
    Both original Submit() methods are unchanged in their behavior so this will not break any users.
---
 CHANGELOG.asciidoc                                 |  2 +
 docs/src/reference/gremlin-applications.asciidoc   |  3 +-
 docs/src/reference/gremlin-variants.asciidoc       | 23 ++++--
 .../Util/SocketServerSettings.cs                   |  2 +-
 gremlin-go/driver/client.go                        | 17 ++++-
 gremlin-go/driver/client_test.go                   | 79 +++++++++++++++++---
 gremlin-go/driver/connection_test.go               |  8 +-
 gremlin-go/driver/driverRemoteConnection.go        | 11 ++-
 gremlin-go/driver/request.go                       | 32 +++++---
 gremlin-go/driver/requestOptions.go                | 85 ++++++++++++++++++++++
 gremlin-go/driver/requestOptions_test.go           | 75 +++++++++++++++++++
 gremlin-go/driver/request_test.go                  | 32 ++++++--
 12 files changed, 319 insertions(+), 50 deletions(-)

diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc
index 19d74303d3..392b04993e 100644
--- a/CHANGELOG.asciidoc
+++ b/CHANGELOG.asciidoc
@@ -36,6 +36,8 @@ This release also includes changes from <<release-3-6-XXX, 3.6.XXX>>.
 * Moves `SimpleSocketServer` and its initializers to a new `gremlin-tools/gremlin-socket-server` module.
 * Configures `gremlin-socket-server` to build a docker image which can be used for testing GLV's. (Can be skipped with -DskipImageBuild)
 * Reduces dependency from `gremlin-server` onto `gremlin-driver` to a test scope only.
+* Added `RequestOptions` and `RequestOptionsBuilder` types to Go GLV to encapsulate per-request settings and bindings.
+* Added `SubmitWithOptions()` methods to `Client` and `DriverRemoteConnection` in Go GLV to pass `RequestOptions` to the server.
 
 == TinkerPop 3.6.0 (Tinkerheart)
 
diff --git a/docs/src/reference/gremlin-applications.asciidoc b/docs/src/reference/gremlin-applications.asciidoc
index 593931980a..6d5d246787 100644
--- a/docs/src/reference/gremlin-applications.asciidoc
+++ b/docs/src/reference/gremlin-applications.asciidoc
@@ -612,7 +612,8 @@ list = g.V().has("person","name","marko").out("knows").toList()
 ----
 // script
 client, err := NewClient("ws://localhost:8182/gremlin")
-resultSet, err := client.Submit("g.V().has('person','name',name).out('knows')", map[string]interface{}{"name": "marko"})
+resultSet, err := client.SubmitWithOptions("g.V().has('person','name',name).out('knows')",
+	new(RequestOptionsBuilder).AddBinding("name", "marko").Create())
 result, err := resultSet.All()
 
 // bytecode
diff --git a/docs/src/reference/gremlin-variants.asciidoc b/docs/src/reference/gremlin-variants.asciidoc
index fcf8f126b1..2d2fc71a2f 100644
--- a/docs/src/reference/gremlin-variants.asciidoc
+++ b/docs/src/reference/gremlin-variants.asciidoc
@@ -293,22 +293,29 @@ fmt.Println(result[0].GetString()) <3>
 ----
 
 <1> Submit a script that simply returns a Count of vertexes.
-<2> Get results from resultSet. Block until the the script is evaluated and results are sent back by the server.
+<2> Get results from resultSet. Block until the script is evaluated and results are sent back by the server.
 <3> Use the result.
 
 ==== Per Request Settings
 
-The `client.Submit()` functions accept a `bindings` which expects a map. The `bindings` provide a way to include options
-that are specific to the request made with the call to `Submit()`. A good use-case for this feature is to set a per-request
-override to the `evaluationTimeout` so that it only applies to the current request.
+Both the `Client` and `DriverRemoteConnection` types have a `SubmitWithOptions(traversalString, requestOptions)` variant
+of the standard `Submit()` method. These methods allow a `RequestOptions` struct to be passed in which will augment the
+execution on the server. `RequestOptions` can be constructed
+using `RequestOptionsBuilder`. A good use-case for this feature is to set a per-request override to the
+`evaluationTimeout` so that it only applies to the current request.
 
 [source,go]
 ----
-resultSet, err := client.Submit("g.V().repeat(both()).times(100)", map[string]interface{}{"evaluationTimeout": 5000})
+options := new(RequestOptionsBuilder).
+			SetEvaluationTimeout(5000).
+			AddBinding("x", 100).
+			Create()
+resultSet, err := client.SubmitWithOptions("g.V(x).count()", options)
 ----
 
 The following options are allowed on a per-request basis in this fashion: `batchSize`, `requestId`, `userAgent` and
-`evaluationTimeout`.
+`evaluationTimeout`. `RequestOptions` may also contain a map of variable `bindings` to be applied to the supplied
+traversal string.
 
 IMPORTANT: The preferred method for setting a per-request timeout for scripts is demonstrated above, but those familiar
 with bytecode may try `g.with("evaluationTimeout", 500)` within a script. Scripts with multiple traversals and multiple
@@ -316,9 +323,9 @@ timeouts will be interpreted as a sum of all timeouts identified in the script f
 
 [source,go]
 ----
-resultSet, err := client.Submit("g.with('evaluationTimeout', 500).addV().iterate();"+
+resultSet, err := client.SubmitWithOptions("g.with('evaluationTimeout', 500).addV().iterate();"+
   "g.addV().iterate();"+
-  "g.with('evaluationTimeout', 500).addV();", map[string]interface{}{"evaluationTimeout": 500})
+  "g.with('evaluationTimeout', 500).addV();", new(RequestOptionsBuilder).SetEvaluationTimeout(500).Create())
 results, err := resultSet.All()
 ----
 
diff --git a/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Util/SocketServerSettings.cs b/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Util/SocketServerSettings.cs
index dae1e40782..2ae0e22c7a 100644
--- a/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Util/SocketServerSettings.cs
+++ b/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Util/SocketServerSettings.cs
@@ -87,7 +87,7 @@ public class SocketServerSettings
     
     public static SocketServerSettings FromYaml(String path)
     {
-        var deserializer = new YamlDotNet.Serialization.DeserializerBuilder().Build();
+        var deserializer = new YamlDotNet.Serialization.DeserializerBuilder().IgnoreUnmatchedProperties().Build();
 
         return deserializer.Deserialize<SocketServerSettings>(File.ReadAllText(path));
     }
diff --git a/gremlin-go/driver/client.go b/gremlin-go/driver/client.go
index 8e2f2cf7d2..872db55773 100644
--- a/gremlin-go/driver/client.go
+++ b/gremlin-go/driver/client.go
@@ -147,10 +147,10 @@ func (client *Client) Close() {
 	client.connections.close()
 }
 
-// Submit submits a Gremlin script to the server and returns a ResultSet.
-func (client *Client) Submit(traversalString string, bindings ...map[string]interface{}) (ResultSet, error) {
+// SubmitWithOptions submits a Gremlin script to the server with specified RequestOptions and returns a ResultSet.
+func (client *Client) SubmitWithOptions(traversalString string, requestOptions RequestOptions) (ResultSet, error) {
 	client.logHandler.logf(Debug, submitStartedString, traversalString)
-	request := makeStringRequest(traversalString, client.traversalSource, client.session, bindings...)
+	request := makeStringRequest(traversalString, client.traversalSource, client.session, requestOptions)
 	result, err := client.connections.write(&request)
 	if err != nil {
 		client.logHandler.logf(Error, logErrorGeneric, "Client.Submit()", err.Error())
@@ -158,6 +158,17 @@ func (client *Client) Submit(traversalString string, bindings ...map[string]inte
 	return result, err
 }
 
+// Submit submits a Gremlin script to the server and returns a ResultSet. Submit can optionally accept a map of bindings
+// to be applied to the traversalString, it is preferred however to instead wrap any bindings into a RequestOptions
+// struct and use SubmitWithOptions().
+func (client *Client) Submit(traversalString string, bindings ...map[string]interface{}) (ResultSet, error) {
+	requestOptionsBuilder := new(RequestOptionsBuilder)
+	if len(bindings) > 0 {
+		requestOptionsBuilder.SetBindings(bindings[0])
+	}
+	return client.SubmitWithOptions(traversalString, requestOptionsBuilder.Create())
+}
+
 // submitBytecode submits Bytecode to the server to execute and returns a ResultSet.
 func (client *Client) submitBytecode(bytecode *Bytecode) (ResultSet, error) {
 	client.logHandler.logf(Debug, submitStartedBytecode, *bytecode)
diff --git a/gremlin-go/driver/client_test.go b/gremlin-go/driver/client_test.go
index fdda8eb614..3e2fd1d286 100644
--- a/gremlin-go/driver/client_test.go
+++ b/gremlin-go/driver/client_test.go
@@ -37,13 +37,14 @@ func TestClient(t *testing.T) {
 	testNoAuthAuthInfo := &AuthInfo{}
 	testNoAuthTlsConfig := &tls.Config{}
 
-	t.Run("Test client.submit()", func(t *testing.T) {
+	t.Run("Test client.Submit()", func(t *testing.T) {
 		skipTestsIfNotEnabled(t, integrationTestSuiteName, testNoAuthEnable)
 		client, err := NewClient(testNoAuthUrl,
 			func(settings *ClientSettings) {
 				settings.TlsConfig = testNoAuthTlsConfig
 				settings.AuthInfo = testNoAuthAuthInfo
 			})
+		defer client.Close()
 		assert.Nil(t, err)
 		assert.NotNil(t, client)
 		resultSet, err := client.Submit("g.V().count()")
@@ -53,7 +54,25 @@ func TestClient(t *testing.T) {
 		assert.Nil(t, err)
 		assert.True(t, ok)
 		assert.NotNil(t, result)
-		client.Close()
+	})
+
+	t.Run("Test client.SubmitWithOptions()", func(t *testing.T) {
+		skipTestsIfNotEnabled(t, integrationTestSuiteName, testNoAuthEnable)
+		client, err := NewClient(testNoAuthUrl,
+			func(settings *ClientSettings) {
+				settings.TlsConfig = testNoAuthTlsConfig
+				settings.AuthInfo = testNoAuthAuthInfo
+			})
+		defer client.Close()
+		assert.Nil(t, err)
+		assert.NotNil(t, client)
+		resultSet, err := client.SubmitWithOptions("g.V().count()", *new(RequestOptions))
+		assert.Nil(t, err)
+		assert.NotNil(t, resultSet)
+		result, ok, err := resultSet.One()
+		assert.Nil(t, err)
+		assert.True(t, ok)
+		assert.NotNil(t, result)
 	})
 }
 
@@ -91,6 +110,12 @@ type SocketServerSettings struct {
 	 * during the web socket handshake.
 	 */
 	USER_AGENT_REQUEST_ID uuid.UUID `yaml:"USER_AGENT_REQUEST_ID"`
+	/**
+	 * If a request with this ID comes to the server, the server responds with a string containing all overridden
+	 * per request settings from the request message. String will be of the form
+	 * "requestId=19436d9e-f8fc-4b67-8a76-deec60918424 evaluationTimeout=1234, batchSize=12, userAgent=testUserAgent"
+	 */
+	PER_REQUEST_SETTINGS_REQUEST_ID uuid.UUID `yaml:"PER_REQUEST_SETTINGS_REQUEST_ID"`
 }
 
 func FromYaml(path string) *SocketServerSettings {
@@ -121,16 +146,17 @@ func TestClientAgainstSocketServer(t *testing.T) {
 	t.Run("Should get single vertex response from gremlin socket server", func(t *testing.T) {
 		skipTestsIfNotEnabled(t, integrationTestSuiteName, testNoAuthEnable)
 		client, err := NewClient(testSocketServerUrl)
+		defer client.Close()
 		assert.Nil(t, err)
 		assert.NotNil(t, client)
-		resultSet, err := client.Submit("1", map[string]interface{}{"requestId": settings.SINGLE_VERTEX_REQUEST_ID})
+		resultSet, err := client.SubmitWithOptions("1", new(RequestOptionsBuilder).
+			SetRequestId(settings.SINGLE_VERTEX_REQUEST_ID).Create())
 		assert.Nil(t, err)
 		assert.NotNil(t, resultSet)
 		result, ok, err := resultSet.One()
 		assert.Nil(t, err)
 		assert.True(t, ok)
 		assert.NotNil(t, result)
-		client.Close()
 	})
 
 	/**
@@ -140,10 +166,12 @@ func TestClientAgainstSocketServer(t *testing.T) {
 	t.Run("Should include user agent in handshake request", func(t *testing.T) {
 		skipTestsIfNotEnabled(t, integrationTestSuiteName, testNoAuthEnable)
 		client, err := NewClient(testSocketServerUrl)
+		defer client.Close()
 		assert.Nil(t, err)
 		assert.NotNil(t, client)
 
-		resultSet, err := client.Submit("1", map[string]interface{}{"requestId": settings.USER_AGENT_REQUEST_ID})
+		resultSet, err := client.SubmitWithOptions("1", new(RequestOptionsBuilder).
+			SetRequestId(settings.USER_AGENT_REQUEST_ID).Create())
 		assert.Nil(t, err)
 		assert.NotNil(t, resultSet)
 
@@ -154,8 +182,6 @@ func TestClientAgainstSocketServer(t *testing.T) {
 
 		userAgentResponse := result.GetString()
 		assert.Equal(t, userAgent, userAgentResponse)
-
-		client.Close()
 	})
 
 	/**
@@ -168,10 +194,12 @@ func TestClientAgainstSocketServer(t *testing.T) {
 			func(settings *ClientSettings) {
 				settings.EnableUserAgentOnConnect = false
 			})
+		defer client.Close()
 		assert.Nil(t, err)
 		assert.NotNil(t, client)
 
-		resultSet, err := client.Submit("1", map[string]interface{}{"requestId": settings.USER_AGENT_REQUEST_ID})
+		resultSet, err := client.SubmitWithOptions("1", new(RequestOptionsBuilder).
+			SetRequestId(settings.USER_AGENT_REQUEST_ID).Create())
 		assert.Nil(t, err)
 		assert.NotNil(t, resultSet)
 
@@ -184,8 +212,33 @@ func TestClientAgainstSocketServer(t *testing.T) {
 		//If the gremlin user agent is disabled, the underlying web socket library reverts to sending its default user agent
 		//during connection requests.
 		assert.Contains(t, userAgentResponse, "Go-http-client/")
+	})
+
+	/**
+	 * Tests that client is correctly sending all overridable per request settings (requestId, batchSize,
+	 * evaluationTimeout, and userAgent) to the server.
+	 */
+	t.Run("Should Send Per Request Settings To Server", func(t *testing.T) {
+		skipTestsIfNotEnabled(t, integrationTestSuiteName, testNoAuthEnable)
+		client, err := NewClient(testSocketServerUrl)
+		defer client.Close()
+		assert.Nil(t, err)
+		assert.NotNil(t, client)
 
-		client.Close()
+		resultSet, err := client.SubmitWithOptions("1", new(RequestOptionsBuilder).
+			SetRequestId(settings.PER_REQUEST_SETTINGS_REQUEST_ID).
+			SetEvaluationTimeout(1234).
+			SetBatchSize(12).
+			SetUserAgent("helloWorld").
+			Create())
+		assert.Nil(t, err)
+		assert.NotNil(t, resultSet)
+		result, ok, err := resultSet.One()
+		assert.Nil(t, err)
+		assert.True(t, ok)
+		expectedResult := fmt.Sprintf("requestId=%v evaluationTimeout=%v, batchSize=%v, userAgent=%v",
+			settings.PER_REQUEST_SETTINGS_REQUEST_ID, 1234, 12, "helloWorld")
+		assert.Equal(t, expectedResult, result.Data)
 	})
 
 	/**
@@ -197,9 +250,11 @@ func TestClientAgainstSocketServer(t *testing.T) {
 		t.Run("Should try create new connection if closed by server", func(t *testing.T) {
 			skipTestsIfNotEnabled(t, integrationTestSuiteName, testNoAuthEnable)
 			client, err := NewClient(testSocketServerUrl)
+			defer client.Close()
 			assert.Nil(t, err)
 			assert.NotNil(t, client)
-			resultSet, err := client.Submit("1", map[string]interface{}{"requestId": settings.CLOSE_CONNECTION_REQUEST_ID})
+			resultSet, err := client.SubmitWithOptions("1", new(RequestOptionsBuilder).
+				SetRequestId(settings.CLOSE_CONNECTION_REQUEST_ID).Create())
 			assert.Nil(t, err)
 			assert.NotNil(t, resultSet)
 
@@ -207,14 +262,14 @@ func TestClientAgainstSocketServer(t *testing.T) {
 
 			assert.EqualError(t, err, "websocket: close 1005 (no status)")
 
-			resultSet, err = client.Submit("1", map[string]interface{}{"requestId": settings.SINGLE_VERTEX_REQUEST_ID})
+			resultSet, err = client.SubmitWithOptions("1", new(RequestOptionsBuilder).
+				SetRequestId(settings.SINGLE_VERTEX_REQUEST_ID).Create())
 			assert.Nil(t, err)
 			assert.NotNil(t, resultSet)
 			result, ok, err = resultSet.One()
 			assert.Nil(t, err)
 			assert.True(t, ok)
 			assert.NotNil(t, result)
-			client.Close()
 		})
 	*/
 }
diff --git a/gremlin-go/driver/connection_test.go b/gremlin-go/driver/connection_test.go
index 9c142d8bb3..b6e0045b2a 100644
--- a/gremlin-go/driver/connection_test.go
+++ b/gremlin-go/driver/connection_test.go
@@ -381,7 +381,7 @@ func TestConnection(t *testing.T) {
 		assert.NotNil(t, connection)
 		assert.Equal(t, established, connection.state)
 		defer deferredCleanup(t, connection)
-		request := makeStringRequest("g.V().count()", "g", "")
+		request := makeStringRequest("g.V().count()", "g", "", *new(RequestOptions))
 		resultSet, err := connection.write(&request)
 		assert.Nil(t, err)
 		assert.NotNil(t, resultSet)
@@ -399,7 +399,7 @@ func TestConnection(t *testing.T) {
 		assert.NotNil(t, connection)
 		assert.Equal(t, established, connection.state)
 		defer deferredCleanup(t, connection)
-		request := makeStringRequest("g.V().count()", "g", "")
+		request := makeStringRequest("g.V().count()", "g", "", *new(RequestOptions))
 		resultSet, err := connection.write(&request)
 		assert.Nil(t, err)
 		assert.NotNil(t, resultSet)
@@ -435,7 +435,7 @@ func TestConnection(t *testing.T) {
 		err = connection.close()
 		assert.Nil(t, err)
 		assert.Equal(t, closed, connection.state)
-		request := makeStringRequest("g.V().count()", "g", "")
+		request := makeStringRequest("g.V().count()", "g", "", *new(RequestOptions))
 		resultSet, err := connection.write(&request)
 		assert.Nil(t, resultSet)
 		assert.Equal(t, newError(err0102WriteConnectionClosedError), err)
@@ -451,7 +451,7 @@ func TestConnection(t *testing.T) {
 		assert.Equal(t, established, connection.state)
 		assert.Nil(t, err)
 		time.Sleep(120 * time.Second)
-		request := makeStringRequest("g.V().count()", "g", "")
+		request := makeStringRequest("g.V().count()", "g", "", *new(RequestOptions))
 		resultSet, err := connection.write(&request)
 		assert.Nil(t, resultSet)
 		assert.NotNil(t, err)
diff --git a/gremlin-go/driver/driverRemoteConnection.go b/gremlin-go/driver/driverRemoteConnection.go
index 4677c6c38d..cce81dbb25 100644
--- a/gremlin-go/driver/driverRemoteConnection.go
+++ b/gremlin-go/driver/driverRemoteConnection.go
@@ -163,15 +163,20 @@ func (driver *DriverRemoteConnection) Close() {
 	driver.isClosed = true
 }
 
-// Submit sends a string traversal to the server.
-func (driver *DriverRemoteConnection) Submit(traversalString string) (ResultSet, error) {
-	result, err := driver.client.Submit(traversalString)
+// SubmitWithOptions sends a string traversal to the server along with specified RequestOptions.
+func (driver *DriverRemoteConnection) SubmitWithOptions(traversalString string, requestOptions RequestOptions) (ResultSet, error) {
+	result, err := driver.client.SubmitWithOptions(traversalString, requestOptions)
 	if err != nil {
 		driver.client.logHandler.logf(Error, logErrorGeneric, "Driver.Submit()", err.Error())
 	}
 	return result, err
 }
 
+// Submit sends a string traversal to the server.
+func (driver *DriverRemoteConnection) Submit(traversalString string) (ResultSet, error) {
+	return driver.SubmitWithOptions(traversalString, *new(RequestOptions))
+}
+
 // submitBytecode sends a Bytecode traversal to the server.
 func (driver *DriverRemoteConnection) submitBytecode(bytecode *Bytecode) (ResultSet, error) {
 	if driver.isClosed {
diff --git a/gremlin-go/driver/request.go b/gremlin-go/driver/request.go
index 304317d9a3..d1cd8a9ee5 100644
--- a/gremlin-go/driver/request.go
+++ b/gremlin-go/driver/request.go
@@ -20,7 +20,6 @@ under the License.
 package gremlingo
 
 import (
-	"fmt"
 	"github.com/google/uuid"
 )
 
@@ -37,8 +36,7 @@ const sessionProcessor = "session"
 const stringOp = "eval"
 const stringProcessor = ""
 
-// Bindings should be a key-object map (different from Binding class in Bytecode).
-func makeStringRequest(stringGremlin string, traversalSource string, sessionId string, bindings ...map[string]interface{}) (req request) {
+func makeStringRequest(stringGremlin string, traversalSource string, sessionId string, requestOptions RequestOptions) (req request) {
 	newProcessor := stringProcessor
 	newArgs := map[string]interface{}{
 		"gremlin": stringGremlin,
@@ -50,13 +48,27 @@ func makeStringRequest(stringGremlin string, traversalSource string, sessionId s
 		newProcessor = sessionProcessor
 		newArgs["session"] = sessionId
 	}
-	requestId := uuid.New()
-	if len(bindings) > 0 {
-		newArgs["bindings"] = bindings[0]
-		customRequestId, err := uuid.Parse(fmt.Sprintf("%v", bindings[0]["requestId"]))
-		if err == nil {
-			requestId = customRequestId
-		}
+	var requestId uuid.UUID
+	if requestOptions.requestID == uuid.Nil {
+		requestId = uuid.New()
+	} else {
+		requestId = requestOptions.requestID
+	}
+
+	if requestOptions.bindings != nil {
+		newArgs["bindings"] = requestOptions.bindings
+	}
+
+	if requestOptions.evaluationTimeout != 0 {
+		newArgs["evaluationTimeout"] = requestOptions.evaluationTimeout
+	}
+
+	if requestOptions.batchSize != 0 {
+		newArgs["batchSize"] = requestOptions.batchSize
+	}
+
+	if requestOptions.userAgent != "" {
+		newArgs["userAgent"] = requestOptions.userAgent
 	}
 
 	return request{
diff --git a/gremlin-go/driver/requestOptions.go b/gremlin-go/driver/requestOptions.go
new file mode 100644
index 0000000000..4153eb162a
--- /dev/null
+++ b/gremlin-go/driver/requestOptions.go
@@ -0,0 +1,85 @@
+/*
+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 gremlingo
+
+import (
+	"github.com/google/uuid"
+)
+
+type RequestOptions struct {
+	requestID         uuid.UUID
+	evaluationTimeout int
+	batchSize         int
+	userAgent         string
+	bindings          map[string]interface{}
+}
+
+type RequestOptionsBuilder struct {
+	requestID         uuid.UUID
+	evaluationTimeout int
+	batchSize         int
+	userAgent         string
+	bindings          map[string]interface{}
+}
+
+func (builder *RequestOptionsBuilder) SetRequestId(requestId uuid.UUID) *RequestOptionsBuilder {
+	builder.requestID = requestId
+	return builder
+}
+
+func (builder *RequestOptionsBuilder) SetEvaluationTimeout(evaluationTimeout int) *RequestOptionsBuilder {
+	builder.evaluationTimeout = evaluationTimeout
+	return builder
+}
+
+func (builder *RequestOptionsBuilder) SetBatchSize(batchSize int) *RequestOptionsBuilder {
+	builder.batchSize = batchSize
+	return builder
+}
+
+func (builder *RequestOptionsBuilder) SetUserAgent(userAgent string) *RequestOptionsBuilder {
+	builder.userAgent = userAgent
+	return builder
+}
+
+func (builder *RequestOptionsBuilder) SetBindings(bindings map[string]interface{}) *RequestOptionsBuilder {
+	builder.bindings = bindings
+	return builder
+}
+
+func (builder *RequestOptionsBuilder) AddBinding(key string, binding interface{}) *RequestOptionsBuilder {
+	if builder.bindings == nil {
+		builder.bindings = make(map[string]interface{})
+	}
+	builder.bindings[key] = binding
+	return builder
+}
+
+func (builder *RequestOptionsBuilder) Create() RequestOptions {
+	requestOptions := new(RequestOptions)
+
+	requestOptions.requestID = builder.requestID
+	requestOptions.evaluationTimeout = builder.evaluationTimeout
+	requestOptions.batchSize = builder.batchSize
+	requestOptions.userAgent = builder.userAgent
+	requestOptions.bindings = builder.bindings
+
+	return *requestOptions
+}
diff --git a/gremlin-go/driver/requestOptions_test.go b/gremlin-go/driver/requestOptions_test.go
new file mode 100644
index 0000000000..6eb6259262
--- /dev/null
+++ b/gremlin-go/driver/requestOptions_test.go
@@ -0,0 +1,75 @@
+/*
+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 gremlingo
+
+import (
+	"github.com/google/uuid"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestRequestOptions(t *testing.T) {
+	t.Run("Test RequestOptionsBuilder with custom requestID", func(t *testing.T) {
+		requestId := uuid.New()
+		r := new(RequestOptionsBuilder).SetRequestId(requestId).Create()
+		assert.Equal(t, requestId, r.requestID)
+	})
+	t.Run("Test RequestOptionsBuilder with custom evaluationTimeout", func(t *testing.T) {
+		r := new(RequestOptionsBuilder).SetEvaluationTimeout(1234).Create()
+		assert.Equal(t, 1234, r.evaluationTimeout)
+	})
+	t.Run("Test RequestOptionsBuilder with custom batchSize", func(t *testing.T) {
+		r := new(RequestOptionsBuilder).SetBatchSize(123).Create()
+		assert.Equal(t, 123, r.batchSize)
+	})
+	t.Run("Test RequestOptionsBuilder with custom userAgent", func(t *testing.T) {
+		r := new(RequestOptionsBuilder).SetUserAgent("TestUserAgent").Create()
+		assert.Equal(t, "TestUserAgent", r.userAgent)
+	})
+	t.Run("Test RequestOptionsBuilder with custom bindings", func(t *testing.T) {
+		bindings := map[string]interface{}{"x": 2, "y": 5}
+		r := new(RequestOptionsBuilder).SetBindings(bindings).Create()
+		assert.Equal(t, bindings, r.bindings)
+	})
+	t.Run("Test RequestOptionsBuilder AddBinding() with no other bindings", func(t *testing.T) {
+		r := new(RequestOptionsBuilder).AddBinding("x", 2).AddBinding("y", 5).Create()
+		expectedBindings := map[string]interface{}{"x": 2, "y": 5}
+		assert.Equal(t, expectedBindings, r.bindings)
+	})
+	t.Run("Test RequestOptionsBuilder AddBinding() overwriting existing key", func(t *testing.T) {
+		r := new(RequestOptionsBuilder).AddBinding("x", 2).AddBinding("x", 5).Create()
+		expectedBindings := map[string]interface{}{"x": 5}
+		assert.Equal(t, expectedBindings, r.bindings)
+	})
+	t.Run("Test RequestOptionsBuilder AddBinding() with existing bindings", func(t *testing.T) {
+		bindings := map[string]interface{}{"x": 2, "y": 5}
+		r := new(RequestOptionsBuilder).SetBindings(bindings).AddBinding("z", 7).Create()
+		expectedBindings := map[string]interface{}{"x": 2, "y": 5, "z": 7}
+		assert.Equal(t, expectedBindings, r.bindings)
+	})
+	t.Run("Test RequestOptionsBuilder SetBinding(...), SetBinding(nil), AddBinding(...)", func(t *testing.T) {
+		bindings := map[string]interface{}{"x": 2, "y": 5}
+		r := new(RequestOptionsBuilder).SetBindings(bindings).
+			SetBindings(nil).AddBinding("z", 7).Create()
+		expectedBindings := map[string]interface{}{"z": 7}
+		assert.Equal(t, expectedBindings, r.bindings)
+	})
+}
diff --git a/gremlin-go/driver/request_test.go b/gremlin-go/driver/request_test.go
index 0e1d8433e8..c388a1273e 100644
--- a/gremlin-go/driver/request_test.go
+++ b/gremlin-go/driver/request_test.go
@@ -20,7 +20,6 @@ under the License.
 package gremlingo
 
 import (
-	"fmt"
 	"github.com/google/uuid"
 	"testing"
 
@@ -29,22 +28,39 @@ import (
 
 func TestRequest(t *testing.T) {
 	t.Run("Test makeStringRequest() with custom requestID", func(t *testing.T) {
-		requestId := fmt.Sprintf("%v", uuid.New())
-		r := makeStringRequest("g.V()", "g", "", map[string]interface{}{"requestId": requestId})
-		assert.Equal(t, requestId, fmt.Sprintf("%v", r.requestID))
+		requestId := uuid.New()
+		r := makeStringRequest("g.V()", "g", "",
+			new(RequestOptionsBuilder).SetRequestId(requestId).Create())
+		assert.Equal(t, requestId, r.requestID)
 	})
 
 	t.Run("Test makeStringRequest() with no bindings", func(t *testing.T) {
-		r := makeStringRequest("g.V()", "g", "")
+		r := makeStringRequest("g.V()", "g", "", *new(RequestOptions))
 		assert.NotNil(t, r.requestID)
 		assert.NotEqual(t, uuid.Nil, r.requestID)
 	})
 
 	t.Run("Test makeStringRequest() with custom evaluationTimeout", func(t *testing.T) {
-		r := makeStringRequest("g.V()", "g", "", map[string]interface{}{"evaluationTimeout": 1234})
+		r := makeStringRequest("g.V()", "g", "",
+			new(RequestOptionsBuilder).SetEvaluationTimeout(1234).Create())
 		assert.NotNil(t, r.requestID)
 		assert.NotEqual(t, uuid.Nil, r.requestID)
-		bindings := r.args["bindings"].(map[string]interface{})
-		assert.Equal(t, 1234, bindings["evaluationTimeout"])
+		assert.Equal(t, 1234, r.args["evaluationTimeout"])
+	})
+
+	t.Run("Test makeStringRequest() with custom batchSize", func(t *testing.T) {
+		r := makeStringRequest("g.V()", "g", "",
+			new(RequestOptionsBuilder).SetBatchSize(123).Create())
+		assert.NotNil(t, r.requestID)
+		assert.NotEqual(t, uuid.Nil, r.requestID)
+		assert.Equal(t, 123, r.args["batchSize"])
+	})
+
+	t.Run("Test makeStringRequest() with custom userAgent", func(t *testing.T) {
+		r := makeStringRequest("g.V()", "g", "",
+			new(RequestOptionsBuilder).SetUserAgent("TestUserAgent").Create())
+		assert.NotNil(t, r.requestID)
+		assert.NotEqual(t, uuid.Nil, r.requestID)
+		assert.Equal(t, "TestUserAgent", r.args["userAgent"])
 	})
 }