Hi all,
I have put together a proposal to standardize the connection options across the
five Gremlin Language Variants (Java, Python, Go, .NET, JavaScript). The goal
is a
consistent, idiomatic set of options with the same names, defaults, and behavior
wherever the underlying HTTP stack genuinely supports it.
=====================================================================
SCOPE
=====================================================================
The proposal is split into two parts:
Part A - HTTP / Connection options: set once when constructing the
connection (transport, security, pooling, lifecycle, serializer).
Part B - Request-related options that are set on the connection but applied
to every request.
True per-request options (evaluationTimeout/timeoutMs, batchSize, bindings,
materializeProperties, requestId, transactionId, etc.) are intentionally left
to a separate task and are not covered here.
=====================================================================
PART A - PROPOSED CHANGES (HTTP / CONNECTION OPTIONS)
=====================================================================
1. Endpoint as a single url
Add a single `url` entry point to Java/.NET (Python/Go/JS already have it).
Multi-host stays on the existing contact-points API.
Proposed changes (before -> after):
Java: host/port/path -> url (+ host/port/path; multi-host via
contact-points)
Python: url -> url
Go: url -> url
.NET: host/port/path -> url (+ host/port/path overload)
JS: url -> url
2. Provider-Defined Types (PDT)
No change. PDTs already replaced the TP3 custom-type mechanism in 4.x and
all five drivers are at parity.
3. Auth - basic
Rename .NET to Auth.Basic (the namespace disambiguates) and move Go's
helpers into an `auth` sub-package so they read auth.Basic(user, pass) -
the direct analog of Java Auth.basic / .NET Auth.Basic. Breaking change to
Go call sites (gremlingo.BasicAuth -> auth.Basic); flat functions removed,
not aliased.
Proposed changes (before -> after):
Java: Auth.basic -> Auth.basic
Python: auth.basic -> auth.basic
Go: BasicAuth -> auth.Basic (new auth sub-package)
.NET: Auth.BasicAuth -> Auth.Basic (was BasicAuth)
JS: auth.basic -> auth.basic
4. Auth - sigv4
Add a credentials-provider variant to Python (the only driver lacking one).
Rename .NET to Auth.Sigv4 and Go to auth.SigV4 (auth sub-package; breaking,
no alias). Caveat: importing Go's auth package links the AWS SDK via SigV4.
Proposed changes (before -> after):
Java: Auth.sigv4 (has creds variant) -> Auth.sigv4 (creds variant)
Python: auth.sigv4 (env-only) -> auth.sigv4 (creds added)
Go: SigV4Auth (has creds variant) -> auth.SigV4 (auth sub-package)
.NET: Auth.SigV4Auth (has creds variant) -> Auth.Sigv4 (was SigV4Auth)
JS: auth.sigv4 (has creds variant) -> auth.sigv4
5. TLS / SSL config (incl. skip-cert)
Standardize on one canonical `ssl` option that accepts the language-native
TLS config (Java SslContext, Python SSLContext, Go *tls.Config, .NET
SslClientAuthenticationOptions). Adds cert support to .NET (was enableSsl +
skip only) and folds Java's scattered keyStore/trustStore builders into one
object. Skip-cert becomes a property of that config (an optional
skipCertValidation shortcut must merge, not overwrite). JS: N/A as a driver
option (native fetch has no TLS surface) - delegated to the Node/undici
runtime; the dead TLS fields are removed (breaking surface change).
Proposed changes (before -> after):
Java: enableSsl + keyStore/trustStore/ciphers -> ssl (SslContext)
Python: ssl_options (SSLContext) -> ssl (ssl.SSLContext)
Go: TlsConfig (*tls.Config) -> Ssl (*tls.Config)
.NET: enableSsl + skip flag only; no cert support -> Ssl
(SslClientAuthenticationOptions)
JS: fields declared but NOT wired up -> via Node/undici dispatcher
(dead fields removed)
6. Max connections / pool size
Unify the name and default to 128 (Python is 8 today -> 128). JS ships a
default dispatcher capped at 128 (was uncapped).
Proposed changes (before -> after):
Java: maxConnectionPoolSize (128) -> maxConnections (128)
Python: pool_size (8) -> max_connections (128, was 8)
Go: MaximumConcurrentConnections (128) -> MaxConnections (128)
.NET: MaxConnectionsPerServer (128) -> MaxConnections (128)
JS: agent field ignored by fetch -> default dispatcher caps at 128
(safety; was uncapped)
7. Idle connection timeout
Unify the name; 180s default. Python gains it. JS: N/A (undici default fine).
Proposed changes (before -> after):
Java: idleConnectionTimeoutMillis -> idleTimeout (180s)
Python: (none) -> idle_timeout (180s)
Go: IdleConnectionTimeout (180s) -> IdleTimeout (180s)
.NET: IdleConnectionTimeout (180s) -> IdleTimeout (180s)
JS: (none) -> via Node/undici, not a driver option (undici default
4s is fine)
8. Connect timeout
Unify the name; 5s default. Java already had it (rename from
connectionSetupTimeoutMillis); .NET 15s -> 5s; Python genuinely gains it.
JS: N/A (runtime default). Document that connectTimeout is a TCP connection
(transport establishment) timeout - it bounds the TCP connect plus TLS
handshake where applicable, not HTTP request/response processing.
Proposed changes (before -> after):
Java: connectionSetupTimeoutMillis -> connectTimeout (5s)
Python: (none) -> connect_timeout (5s)
Go: ConnectionTimeout (5s) -> ConnectTimeout (5s)
.NET: ConnectionTimeout (15s) -> ConnectTimeout (5s, was 15s)
JS: (none) -> via Node/undici, not a driver option (undici default
10s is fine)
9. Keep-alive (idle time before TCP probes)
Promote to a single canonical keepAliveTime (30s): the idle time before TCP
keep-alive probes begin, so the pool can detect and evict dead/half-open
connections (matters when the idle-connection timeout is large). All five
can express it, each via a different mechanism: Go net.Dialer (today); .NET
a ConnectCallback socket option (rewired from the inert HTTP/2 ping); Python
aiohttp socket_options; JS an undici dispatcher setKeepAlive (idle-delay
only); Java Netty NioChannelOption / TCP_KEEPIDLE (JDK 11+, Linux/macOS).
Probe interval/count stay at OS defaults.
Proposed changes (before -> after):
Java: (none) -> keepAliveTime (NioChannelOption; Linux/macOS)
Python: (none) -> keep_alive_time (TCPConnector socket_options)
Go: KeepAliveInterval (30s) -> KeepAliveTime (net.Dialer)
.NET: HTTP/2 ping, inert on HTTP/1.1 -> KeepAliveTime (ConnectCallback
socket option)
JS: (none) -> keepAliveTime (undici setKeepAlive; idle-delay)
10. Read timeout (NEW)
Add a streaming-safe idle-read timeout in all five (Java ReadTimeoutHandler,
Go SetReadDeadline reset, .NET per-read CancelAfter, JS undici bodyTimeout;
Python read_timeout already works). Partial mitigation for the removed
maxResponseContentLength (bounds idle time, not total bytes).
Proposed changes (before -> after):
Java: (none) -> readTimeout (new; via ReadTimeoutHandler)
Python: read_timeout/write_timeout -> read_timeout (via async_timeout
wrapper)
Go: (none) -> ReadTimeout (new; via SetReadDeadline)
.NET: via CancellationToken -> ReadTimeout (new; via per-read
CancelAfter)
JS: (none) -> readTimeout (new; via undici bodyTimeout)
11. Max response content length (REMOVED)
Remove from all five. It was a total-bytes cap on a now-streamed body;
default was unlimited and it fired mid-stream on large results. Trade-off:
drops an opt-in DoS/memory guard. The read timeout is a partial mitigation;
a true size guard could return later as an explicit incremental-count limit.
Proposed changes (before -> after):
Java: maxResponseContentLength -> removed
Python: accepted but discarded -> removed
Go: (none) -> removed
.NET: (none) -> removed
JS: (none) -> removed
12. Max response header size (NEW)
Expose a canonical maxResponseHeaderBytes (bytes) on Java/Go/.NET/JS,
converting internally (.NET's native unit is KB). Python is the exception:
aiohttp has only per-field/per-line caps (max_field_size/max_line_size,
aiohttp >= 3.9), no byte-total - keep its native options.
Proposed changes (before -> after):
Java: hardcoded 8192 (Netty) -> maxResponseHeaderBytes (canonical; via
Netty codec)
Python: max_field_size/max_line_size (aiohttp >=3.9); not surfaced ->
max_field_size/max_line_size (>=3.9; per-field, no byte-total - canonical N/A)
Go: Transport default 10MB, not exposed -> MaxResponseHeaderBytes
(canonical; native bytes)
.NET: handler default 64KB, not exposed -> MaxResponseHeaderBytes
(canonical; bytes - converts to native KB)
JS: undici default 16KB, not exposed -> maxResponseHeaderBytes
(canonical; via undici maxHeaderSize)
13. HTTP proxy (NEW)
Expose an explicit proxy option on all five (highest demand). Fixes Go
silently dropping the env-var proxy today.
Proposed changes (before -> after):
Java: Netty: not exposed -> HttpProxyHandler (new; pipeline)
Python: aiohttp proxy=, but not surfaced by driver -> proxy= / trust_env
Go: custom Transport drops env proxy (Proxy unset) ->
Transport.Proxy = ProxyFromEnvironment (fix)
.NET: inherits env-aware DefaultProxy; no explicit option ->
SocketsHttpHandler.Proxy / DefaultProxy
JS: fetch: no dispatcher passed -> undici ProxyAgent on default
dispatcher (new)
14. Compression
Standardize one optional `compression` option, default none (off, opt-in);
set deflate to enable. Deflate is text compression, so on already-compressed
or incompressible/tiny payloads it adds CPU/latency for no wire-size win -
hence off by default. Make it algorithm-valued (an extensible enum / typed
const), not a plain boolean, with boolean accepted as sugar. Deflate is the
only algorithm the server negotiates today; gzip/br/zstd are reserved and
added server-side first. Behavior change for Java (sends deflate
unconditionally today). Rename for Go/.NET (EnableCompression ->
Compression); new option for Java/Python/JS.
Proposed changes (before -> after):
Java: - (on, no toggle) -> Compression enum (default off)
Python: (none) -> compression str/enum (new)
Go: EnableCompression (default false) -> Compression const (default
off)
.NET: EnableCompression (default false) -> Compression enum (default
off)
JS: (none) -> compression string union (new)
15. Logging
Canonical `logger` injection point on all five, accepting each language's
logging abstraction: Go Logger (+ its LogVerbosity enum, the only built-in
level), .NET loggerFactory (ILoggerFactory), Java the SLF4J binding, Python
stdlib logging (gremlinpython logger); JS adds a new logger option
(non-trivial - no logging foundation today). Verbosity is set via each
stack's native levels, not a canonical driver enum.
Proposed changes (before -> after):
Java: via slf4j -> logger = SLF4J binding
Python: (none) -> logger = stdlib logging
Go: LogVerbosity + Logger -> Logger + LogVerbosity
.NET: loggerFactory -> loggerFactory (ILoggerFactory)
JS: (none) -> logger (new; non-trivial)
16. Default batch size (connection-level) (NEW)
Add a canonical connection-level defaultBatchSize on all five (Java renames
resultIterationBatchSize; Python/Go/.NET/JS gain it). It is the default for
the per-request batchSize when the caller does not set one - pure
client-side default-filling, no wire or server change. Default 64.
Proposed changes (before -> after):
Java: resultIterationBatchSize (64) -> defaultBatchSize (rename
resultIterationBatchSize; 64)
Python: (none) -> default_batch_size (new)
Go: (none) -> DefaultBatchSize (new)
.NET: (none) -> DefaultBatchSize (new)
JS: (none) -> defaultBatchSize (new)
=====================================================================
PART A - KEPT LANGUAGE-SPECIFIC (NOT STANDARDIZED)
=====================================================================
These are real, live options but have no cross-GLV analog, so they stay
driver-specific rather than being forced onto the others:
- Validation request (Java only): a Gremlin health-check ping used when a host
is marked unavailable; Java's self-managed pool runs it before returning a
host to rotation. The OS-managed stacks handle connection health themselves.
(There is also a default bug to reconcile: Builder default '' vs Settings
default g.inject(0).)
- Java-only pool knobs: maxWaitForClose and reconnectInterval are tied to
Java's self-managed Netty pool. (maxWaitForConnection is conceptually
universal but is subsumed into the connect/request timeout elsewhere.)
- Thread/IO pool sizing: Java nioPoolSize/workerPoolSize and Python max_workers
are live tuning knobs but tie to Java's Netty pool and Python's
thread-per-connection model; no analog in the OS-managed stacks. Each knob
needs user-facing tuning docs - what it sizes, its default, and when to raise
vs lower it.
- Max idle connections (Go only): MaxIdleConnections is a count-based
pool-retention policy unique to Go's transport; the others bound idle
connections by time, not count, and its purpose is already covered by idle
timeout + max connections.
=====================================================================
PART B - REQUEST-RELATED OPTIONS (SET ON CONNECTION, APPLIED PER REQUEST)
=====================================================================
- Configurable traversal source: already everywhere; Java addG ->
traversalSource rename, .NET casing change.
Proposed changes (before -> after):
Java: addG -> traversalSource
Python: traversal_source -> traversal_source
Go: TraversalSource -> TraversalSource
.NET: traversalSource -> TraversalSource
JS: traversalSource -> traversalSource
- Custom headers: must be set through a RequestInterceptor (the single
canonical mechanism, present in all five). Drop the standalone `headers`
option (Python kwarg, JS option) as redundant - breaking for Python/JS;
migration is a one-line header-setting interceptor. Interceptors run before
auth is appended, so SigV4 still signs over user headers.
Proposed changes (before -> after):
Java: via interceptor -> interceptor
Python: headers kwarg -> interceptor (headers kwarg dropped)
Go: via interceptor -> interceptor
.NET: via interceptor -> interceptor
JS: headers option -> interceptor (headers option dropped)
- Request interceptors: standardize on a plain `interceptors` list; Java keeps
its named-chain helpers.
Proposed changes (before -> after):
Java: named chain -> interceptors (+ named-chain helpers)
Python: list -> interceptors
Go: slice -> Interceptors (was RequestInterceptors)
.NET: list -> Interceptors
JS: single/array -> interceptors
- enableUserAgentOnConnect: already uniform; no change.
- bulkResults: already supported per-request in all five. Java/Python/.NET also
have a connection-level flag; Go/JS keep it per-request only. No functional
change - just standardizing the per-request option as canonical.
Proposed changes (before -> after):
Java: bulkResults (conn + per-req) -> bulkResults (conn + per-req)
Python: bulk_results (conn + per-req) -> bulk_results (conn + per-req)
Go: per-request only (no conn-level flag) -> BulkResults
(per-request only)
.NET: BulkResults (conn + per-req) -> BulkResults (conn + per-req)
JS: per-request only (no conn-level flag) -> bulkResults
(per-request only)
=====================================================================
NOTABLE IMPLEMENTATION CAVEATS
=====================================================================
- JS adopts undici as a new explicit dependency (the driver is fetch-only
today) to enable the pool cap, bodyTimeout, maxHeaderSize, and proxy.
- Go: server compression is per-GraphBinary-chunk, not generic HTTP
compression, so Go must keep its explicit deflate decode. Also, the custom
Transport currently drops the env proxy (fix with ProxyFromEnvironment).
- .NET keep-alive must be rewired from the inert HTTP/2 KeepAlivePingTimeout to
a ConnectCallback TCP socket option; skip-cert must merge into, not overwrite,
the user's SslClientAuthenticationOptions.
- Java keep-alive (TCP_KEEPIDLE) is JDK 11+ and platform-dependent
(Linux/macOS); the read-timeout handler must be armed per request.
- Header-size unit differs (.NET KB vs Go/JS bytes) - document to avoid a 1024x
misconfiguration.
Thanks,
Guian