From 3235232ddae13c012af1a6bc89bb329aed0f96f4 Mon Sep 17 00:00:00 2001 From: Miles Bryant Date: Thu, 26 Mar 2026 11:11:44 +0000 Subject: [PATCH 1/3] Add HTTPTransport setting to gremlin-go driver MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Allow users to provide a custom http.RoundTripper for Gremlin HTTP requests. This enables transport-level instrumentation (connection pool monitoring, request tracing via net/http/httptrace, metrics) without modifying driver internals. When HTTPTransport is set, the driver uses it directly and ignores pool-related settings (MaximumConcurrentConnections, MaxIdleConnections, etc.) since those configure the default transport. When nil, behaviour is unchanged — the driver creates its own http.Transport with the configured pool settings. Co-Authored-By: Claude Opus 4.6 --- gremlin-go/driver/client.go | 14 ++++ gremlin-go/driver/connection.go | 29 +++++--- gremlin-go/driver/connection_test.go | 74 +++++++++++++++++++++ gremlin-go/driver/driverRemoteConnection.go | 14 ++++ 4 files changed, 121 insertions(+), 10 deletions(-) diff --git a/gremlin-go/driver/client.go b/gremlin-go/driver/client.go index 4d799b878d1..6bff7db238d 100644 --- a/gremlin-go/driver/client.go +++ b/gremlin-go/driver/client.go @@ -21,6 +21,7 @@ package gremlingo import ( "crypto/tls" + "net/http" "reflect" "time" @@ -61,6 +62,18 @@ type ClientSettings struct { EnableUserAgentOnConnect bool + // HTTPTransport is the http.RoundTripper used for Gremlin HTTP requests. + // + // When set, the driver uses this transport directly. The following settings + // are ignored as they configure the driver's default transport: + // MaximumConcurrentConnections, MaxIdleConnections, IdleConnectionTimeout, + // KeepAliveInterval, TlsConfig, and EnableCompression. Configure these on + // your transport directly. + // + // When nil (the default), the driver creates an http.Transport configured + // with the above settings. + HTTPTransport http.RoundTripper + // RequestInterceptors are functions that modify HTTP requests before sending. RequestInterceptors []RequestInterceptor } @@ -105,6 +118,7 @@ func NewClient(url string, configurations ...func(settings *ClientSettings)) (*C keepAliveInterval: settings.KeepAliveInterval, enableCompression: settings.EnableCompression, enableUserAgentOnConnect: settings.EnableUserAgentOnConnect, + httpTransport: settings.HTTPTransport, } logHandler := newLogHandler(settings.Logger, settings.LogVerbosity, settings.Language) diff --git a/gremlin-go/driver/connection.go b/gremlin-go/driver/connection.go index 184efb681bb..cdfd52b260d 100644 --- a/gremlin-go/driver/connection.go +++ b/gremlin-go/driver/connection.go @@ -95,6 +95,7 @@ type connectionSettings struct { keepAliveInterval time.Duration enableCompression bool enableUserAgentOnConnect bool + httpTransport http.RoundTripper } // connection handles HTTP request/response for Gremlin queries. @@ -118,7 +119,23 @@ const ( ) func newConnection(handler *logHandler, url string, connSettings *connectionSettings) *connection { - // Apply defaults for zero values + var rt http.RoundTripper + if connSettings.httpTransport != nil { + rt = connSettings.httpTransport + } else { + rt = newDefaultTransport(connSettings) + } + + return &connection{ + url: url, + httpClient: &http.Client{Transport: rt}, // No Timeout - allows streaming + connSettings: connSettings, + logHandler: handler, + serializer: newGraphBinarySerializer(handler), + } +} + +func newDefaultTransport(connSettings *connectionSettings) *http.Transport { connectionTimeout := connSettings.connectionTimeout if connectionTimeout == 0 { connectionTimeout = defaultConnectionTimeout @@ -144,7 +161,7 @@ func newConnection(handler *logHandler, url string, connSettings *connectionSett keepAliveInterval = defaultKeepAliveInterval } - transport := &http.Transport{ + return &http.Transport{ DialContext: (&net.Dialer{ Timeout: connectionTimeout, KeepAlive: keepAliveInterval, @@ -155,14 +172,6 @@ func newConnection(handler *logHandler, url string, connSettings *connectionSett IdleConnTimeout: idleConnTimeout, DisableCompression: !connSettings.enableCompression, } - - return &connection{ - url: url, - httpClient: &http.Client{Transport: transport}, // No Timeout - allows streaming - connSettings: connSettings, - logHandler: handler, - serializer: newGraphBinarySerializer(handler), - } } // AddInterceptor adds a request interceptor to the chain. diff --git a/gremlin-go/driver/connection_test.go b/gremlin-go/driver/connection_test.go index a222448f2c1..2e5d70fbcfd 100644 --- a/gremlin-go/driver/connection_test.go +++ b/gremlin-go/driver/connection_test.go @@ -1134,3 +1134,77 @@ func TestDriverRemoteConnectionSettingsWiring(t *testing.T) { assert.Equal(t, 180*time.Second, transport.IdleConnTimeout) }) } + +// countingRoundTripper is a test RoundTripper that counts requests. +type countingRoundTripper struct { + base http.RoundTripper + count int +} + +func (c *countingRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { + c.count++ + return c.base.RoundTrip(req) +} + +func TestHTTPTransport(t *testing.T) { + t.Run("custom transport is used for requests", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + })) + defer server.Close() + + counting := &countingRoundTripper{base: http.DefaultTransport} + conn := newConnection(newTestLogHandler(), server.URL, &connectionSettings{ + httpTransport: counting, + }) + + rs, err := conn.submit(&request{gremlin: "g.V()", fields: map[string]interface{}{}}) + require.NoError(t, err) + _, _ = rs.All() // drain + + assert.Equal(t, 1, counting.count, "custom transport should have been called") + }) + + t.Run("nil transport uses default http.Transport", func(t *testing.T) { + conn := newConnection(newTestLogHandler(), "http://localhost:8182/gremlin", &connectionSettings{}) + + // When no custom transport is set, the driver creates an *http.Transport + _, ok := conn.httpClient.Transport.(*http.Transport) + assert.True(t, ok, "default transport should be *http.Transport") + }) + + t.Run("custom transport ignores pool settings", func(t *testing.T) { + counting := &countingRoundTripper{base: http.DefaultTransport} + conn := newConnection(newTestLogHandler(), "http://localhost:8182/gremlin", &connectionSettings{ + httpTransport: counting, + maxConnsPerHost: 256, // should be ignored + }) + + // The transport should be our custom one, not an http.Transport with pool settings + assert.Equal(t, counting, conn.httpClient.Transport, "custom transport should be used directly") + }) + + t.Run("ClientSettings wires HTTPTransport", func(t *testing.T) { + counting := &countingRoundTripper{base: http.DefaultTransport} + client, err := NewClient("http://localhost:8182/gremlin", + func(settings *ClientSettings) { + settings.HTTPTransport = counting + }) + require.NoError(t, err) + defer client.Close() + + assert.Equal(t, counting, client.conn.httpClient.Transport, "custom transport should be wired through") + }) + + t.Run("DriverRemoteConnectionSettings wires HTTPTransport", func(t *testing.T) { + counting := &countingRoundTripper{base: http.DefaultTransport} + drc, err := NewDriverRemoteConnection("http://localhost:8182/gremlin", + func(settings *DriverRemoteConnectionSettings) { + settings.HTTPTransport = counting + }) + require.NoError(t, err) + defer drc.Close() + + assert.Equal(t, counting, drc.client.conn.httpClient.Transport, "custom transport should be wired through") + }) +} diff --git a/gremlin-go/driver/driverRemoteConnection.go b/gremlin-go/driver/driverRemoteConnection.go index 7239b8d5c46..68d0116b1cd 100644 --- a/gremlin-go/driver/driverRemoteConnection.go +++ b/gremlin-go/driver/driverRemoteConnection.go @@ -21,6 +21,7 @@ package gremlingo import ( "crypto/tls" + "net/http" "time" "golang.org/x/text/language" @@ -37,6 +38,18 @@ type DriverRemoteConnectionSettings struct { EnableCompression bool EnableUserAgentOnConnect bool + // HTTPTransport is the http.RoundTripper used for Gremlin HTTP requests. + // + // When set, the driver uses this transport directly. The following settings + // are ignored as they configure the driver's default transport: + // MaximumConcurrentConnections, MaxIdleConnections, IdleConnectionTimeout, + // KeepAliveInterval, TlsConfig, and EnableCompression. Configure these on + // your transport directly. + // + // When nil (the default), the driver creates an http.Transport configured + // with the above settings. + HTTPTransport http.RoundTripper + // MaximumConcurrentConnections is the maximum number of concurrent TCP connections // to the Gremlin server. This limits how many requests can be in-flight simultaneously. // Default: 128. Set to 0 to use the default. @@ -103,6 +116,7 @@ func NewDriverRemoteConnection( keepAliveInterval: settings.KeepAliveInterval, enableCompression: settings.EnableCompression, enableUserAgentOnConnect: settings.EnableUserAgentOnConnect, + httpTransport: settings.HTTPTransport, } logHandler := newLogHandler(settings.Logger, settings.LogVerbosity, settings.Language) From a602ea50d3e49b24d401011af8950595f056bde6 Mon Sep 17 00:00:00 2001 From: Miles Bryant Date: Thu, 26 Mar 2026 11:25:40 +0000 Subject: [PATCH 2/3] Add comment --- gremlin-go/driver/connection.go | 1 + 1 file changed, 1 insertion(+) diff --git a/gremlin-go/driver/connection.go b/gremlin-go/driver/connection.go index cdfd52b260d..57d25980d6a 100644 --- a/gremlin-go/driver/connection.go +++ b/gremlin-go/driver/connection.go @@ -121,6 +121,7 @@ const ( func newConnection(handler *logHandler, url string, connSettings *connectionSettings) *connection { var rt http.RoundTripper if connSettings.httpTransport != nil { + // user supplied their own transport, so use it rt = connSettings.httpTransport } else { rt = newDefaultTransport(connSettings) From 7886e8f0bf207cb3a01c0f773fa4a6efcaab3527 Mon Sep 17 00:00:00 2001 From: Miles Bryant Date: Thu, 26 Mar 2026 11:32:01 +0000 Subject: [PATCH 3/3] Add CHANGELOG entry for HTTPTransport setting Generated-by: Claude Code (claude-opus-4-6) Co-Authored-By: Claude Opus 4.6 --- CHANGELOG.asciidoc | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc index 31176d6d869..b10f532e969 100644 --- a/CHANGELOG.asciidoc +++ b/CHANGELOG.asciidoc @@ -25,6 +25,7 @@ image::https://raw.githubusercontent.com/apache/tinkerpop/master/docs/static/ima [[release-4-0-0]] === TinkerPop 4.0.0 (NOT OFFICIALLY RELEASED YET) +* Added `HTTPTransport` setting to `gremlin-go` to allow users to provide a custom `http.RoundTripper` for transport-level instrumentation. * Added grammar-based `Translator` for `gremlin-javascript` supporting translation to JavaScript, Python, Go, .NET, Java, Groovy, canonical, and anonymized output. * Added `translate_gremlin_query` tool to `gremlin-mcp` that translates Gremlin queries to a target language variant, with optional LLM-assisted normalization via MCP sampling for non-canonical input. * Modified `gremlin-mcp` to support offline mode where utility tools (translate, format) remain available without a configured `GREMLIN_MCP_ENDPOINT`.