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`. 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..57d25980d6a 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,24 @@ const ( ) func newConnection(handler *logHandler, url string, connSettings *connectionSettings) *connection { - // Apply defaults for zero values + var rt http.RoundTripper + if connSettings.httpTransport != nil { + // user supplied their own transport, so use it + 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 +162,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 +173,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)