Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions CHANGELOG.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,20 @@ image::https://raw.githubusercontent.com/apache/tinkerpop/master/docs/static/ima
[[release-4-0-0]]
=== TinkerPop 4.0.0 (Release Date: NOT OFFICIALLY RELEASED YET)

* Standardized `gremlin-javascript` connection options per the TinkerPop 4.x GLV proposal: *(breaking)*
** Adopted `undici` as a pinned dependency that ships a default dispatcher built from the discrete connection options below.
** Added `maxConnections` (default 128), which caps concurrent connections per origin where it was previously uncapped. *(breaking)*
** Added `readTimeoutMillis`, a per-read idle (body) timeout in milliseconds applied to the default dispatcher.
** Added `maxResponseHeaderBytes`, the maximum size of the response headers in bytes applied to the default dispatcher.
** Added `keepAliveTimeMillis` (default 30000), the idle time in milliseconds before TCP keep-alive probes begin, applied to the default dispatcher via a `SO_KEEPALIVE` connector; set to `0` to disable.
** Added `proxy`, which routes requests through an undici `ProxyAgent`.
** Added `compression`, a `'none'`/`'deflate'` string union defaulting to `'deflate'` (on). *(breaking)*
** Added `batchSize` (default 64), a connection-level default that fills a request's `batchSize` when it is left unset.
** Added a connection-level `bulkResults` default, applied to every request unless overridden per-request.
** Added `logger`, a logger object or callback (logging is disabled when unset).
** Removed the `ca`, `cert`, `pfx`, `rejectUnauthorized`, and `agent` options; TLS is now configured via the Node/undici runtime and the HTTP agent is owned by the driver's dispatcher. *(breaking)*
** Removed the standalone `headers` option; set custom headers via an interceptor instead.
* Fixed `gremlin-javascript` `Client.submit()` so that an explicit `bulkResults: false` request option is forwarded to the server instead of being silently dropped.
* Added configurable CORS `allowedOrigins` setting to Gremlin Server; warns when wildcard origin is used alongside authentication.
* Fixed `ByteBuf` leak in `GraphBinaryMessageSerializerV4` when serialization throws an `IOException`.
* Changed `Tree` to no longer extend `HashMap`; it is now a final class with a tree-shaped API (`childAt`, `hasChild`, `contains`, `findSubtree`, `getOrCreateChild`, `getNodesAtDepth`, `getLeafNodes`, `nodeCount`) and is no longer a `Map`.
Expand Down
34 changes: 29 additions & 5 deletions docs/src/reference/gremlin-variants.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -1872,18 +1872,42 @@ can be passed in the constructor of a new `Client` or `DriverRemoteConnection` :
|Key |Type |Description |Default
|url |String |The resource uri. |None
|options |Object |The connection options. |{}
|options.traversalSource |String |The traversal source. |'g'
|options.headers |Object |Additional HTTP header key/values included with each request. |undefined
|options.interceptors |RequestInterceptor/RequestInterceptor[] |One or more functions that can modify the HTTP request before it is sent. |undefined
|options.traversalSource |String |The name of the remote `GraphTraversalSource`. |'g'
|options.maxConnections |Number |Caps the number of concurrent connections per origin on the default dispatcher. |128
|options.readTimeoutMillis |Number |A per-read idle timeout in milliseconds (undici `bodyTimeout`). It resets per chunk, so it is safe for streaming. |runtime default
|options.maxResponseHeaderBytes |Number |The maximum size of the response headers in bytes (undici `maxHeaderSize`). |runtime default
|options.keepAliveTimeMillis |Number |Idle time in milliseconds before TCP keep-alive probes begin. Enables `SO_KEEPALIVE` on the socket. Set to `0` to disable. |30000
|options.proxy |String |An HTTP proxy URI. When set, requests are routed through an undici `ProxyAgent`. |undefined
|options.compression |String |Response compression codec, `'none'` or `'deflate'`. |'deflate'
|options.batchSize |Number |Connection-level default that fills a request's `batchSize` when it is left unset. |64
|options.bulkResults |Boolean |Connection-level default for `bulkResults`, applied to every request unless overridden per-request. The `DriverRemoteConnection` traversal path defaults to `true` regardless of this setting. |false
|options.logger |Object/Function |A logger object (with `debug`/`info`/`warn`/`error` methods, e.g. `console`) or a `(level, message, ...args)` callback. |none (disabled)
|options.interceptors |RequestInterceptor/RequestInterceptor[] |One or more functions that can modify the HTTP request before it is sent, run in order. |undefined
|options.auth |RequestInterceptor |An auth interceptor that is always appended to the end of the interceptor list so it runs last. |undefined
|options.preciseNumbers |Boolean |When `true`, wraps deserialized numbers in typed wrappers that preserve the server's original type. |undefined
|options.reader |GraphBinaryReader |The reader to use for deserializing responses. |GraphBinaryReader
|options.writer |GraphBinaryWriter |The writer to use for serializing requests. |GraphBinaryWriter
|options.enableUserAgentOnConnect |Boolean |Determines if a user agent header will be sent with requests. |true
|options.agent |Agent |A custom `node:http` or `node:https` Agent for connection pooling or proxy configuration. |undefined
|options.pdtRegistry |ProviderDefinedTypeRegistry |A registry for hydrating and dehydrating <<gremlin-javascript-pdt,Provider Defined Types>>. |undefined
|=========================================================

TLS is configured through the Node.js/undici runtime (for example the `NODE_EXTRA_CA_CERTS` or
`NODE_TLS_REJECT_UNAUTHORIZED` environment variables), not through driver options. Custom headers are set via an
interceptor rather than a `headers` option, for example `interceptors: (req) => { req.headers['X-Custom'] = 'value'; }`.

Note that no driver timeout bounds the *total* duration of a request once it is under way. `readTimeoutMillis` only bounds
the gap between response chunks, so a response that keeps producing chunks will not time out no matter how long it
runs overall, and there is no client-side "overall" request timeout. If you need an absolute deadline, impose it in
your application around the call by racing the `submit` promise against a timeout:

[source,javascript]
----
// bound the entire request to 30 seconds
const results = await Promise.race([
client.submit('g.V().out().out()'),
new Promise((_, reject) => setTimeout(() => reject(new Error('request timed out')), 30000)),
]);
----

[[gremlin-javascript-interceptors]]
=== RequestInterceptor

Expand Down
42 changes: 42 additions & 0 deletions docs/src/upgrade/release-4.x.x.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,48 @@ complete list of all the modifications that are part of this release.

=== Upgrading for Users

==== Standardizing JavaScript Connection Options

TinkerPop 4.x standardizes connection option names and defaults across the GLVs. In `gremlin-javascript`, the driver
now adopts `undici` as a pinned dependency that ships a default dispatcher, several options have been removed
(including the standalone `headers` option), and a number of new options have been added. The notes below describe
the JavaScript changes. See <<glv-driver-changes, GLV Driver Changes>> for the equivalent changes in the other
drivers.

Removed options. The following option has been removed:

- `headers` has been removed. Set custom headers via an interceptor instead, e.g.
`interceptors: (req) => { req.headers['X-Custom'] = 'value'; }`. Because auth runs as the last interceptor, SigV4 still
signs over headers added this way.

Behavior changes. These change runtime behavior on upgrade, even if you do not change your configuration:

- `compression` now defaults to `'deflate'` (on), so the driver sends `Accept-Encoding: deflate` by default. It is a
`'none'`/`'deflate'` string union. Set `'none'` to disable it.
- `maxConnections` now caps concurrent connections per origin at 128 by default, where the number of connections was
previously uncapped.

New options:

- `readTimeoutMillis`: a per-read idle (body) timeout in milliseconds applied to the default dispatcher (undici
`bodyTimeout`). It resets per chunk, so it is safe for streaming.
- `maxResponseHeaderBytes`: the maximum size of the response headers in bytes applied to the default dispatcher (undici
`maxHeaderSize`).
- `keepAliveTimeMillis` (default 30000): the idle time in milliseconds before TCP keep-alive probes begin, applied to
the default dispatcher via a `SO_KEEPALIVE` connector. Set 0 to disable.
- `proxy`: an HTTP proxy URI that routes requests through an undici `ProxyAgent`.
- `batchSize` (default 64): a connection-level default that fills a request's `batchSize` when it is left unset.
- `bulkResults` (default false): a connection-level default for `bulkResults` applied to every request unless
overridden per-request. The `DriverRemoteConnection` traversal path defaults to `true` regardless of this setting.
- `logger`: a logger object (with `debug`/`info`/`warn`/`error` methods) or a `(level, message, ...args)` callback.
Logging is disabled when unset.

The `ca`, `cert`, `pfx`, `rejectUnauthorized`, and `agent` options have been removed. TLS is now configured through the
Node/undici runtime (for example `NODE_EXTRA_CA_CERTS` or `NODE_TLS_REJECT_UNAUTHORIZED`) and the HTTP agent is owned
by the driver's dispatcher. *(breaking)*

See: link:https://lists.apache.org/thread/yqtr2wnb1kq2pqqq4002cz511q5o0bkg[[DISCUSS] Standardizing GLV connection options in TinkerPop 4].

===== Declarative Pattern Matching

Gremlin has always offered both imperative and declarative styles to writing graph queries. While the imperative style
Expand Down
11 changes: 6 additions & 5 deletions gremlin-examples/gremlin-javascript/connections.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ under the License.
const gremlin = require('gremlin');
const traversal = gremlin.process.AnonymousTraversalSource.traversal;
const DriverRemoteConnection = gremlin.driver.DriverRemoteConnection;
const serializer = gremlin.structure.io.graphserializer;

const serverUrl = 'ws://localhost:8182/gremlin';
const vertexLabel = 'connection';
Expand All @@ -47,11 +46,13 @@ async function withRemote() {
async function withConfigs() {
// Connecting and customizing configurations
const dc = new DriverRemoteConnection(serverUrl, {
mimeType: 'application/vnd.gremlin-v3.0+json',
reader: serializer,
writer: serializer,
rejectUnauthorized: false,
traversalSource: 'g',
// Optionally tune the underlying connection pool and timeouts.
maxConnections: 64,
readTimeout: 30000,
// TLS is configured through the Node/undici runtime, not via driver
// options: e.g. set NODE_EXTRA_CA_CERTS to trust a custom CA, or
// NODE_TLS_REJECT_UNAUTHORIZED=0 to relax certificate verification.
});
const g = traversal().withRemote(dc);

Expand Down
11 changes: 10 additions & 1 deletion gremlin-js/gremlin-javascript/lib/driver/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,8 +126,17 @@ export default class Client {
if (requestOptions?.evaluationTimeout) {
requestBuilder.addTimeoutMillis(requestOptions.evaluationTimeout);
}
if (requestOptions?.bulkResults) {
// Per-request value wins (including an explicit `false`); otherwise apply the
// connection-level default only when it is true.
if (requestOptions?.bulkResults !== undefined) {
requestBuilder.addBulkResults(requestOptions.bulkResults);
} else if (this._connection.bulkResults) {
requestBuilder.addBulkResults(true);
}
// Fill the per-request batchSize from the connection-level default when unset.
const batchSize = requestOptions?.batchSize ?? this._connection.batchSize;
if (batchSize !== undefined) {
requestBuilder.addBatchSize(batchSize);
}
if (requestOptions?.transactionId) {
requestBuilder.addField('transactionId', requestOptions.transactionId);
Expand Down
112 changes: 94 additions & 18 deletions gremlin-js/gremlin-javascript/lib/driver/connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@

import { Buffer } from 'buffer';
import { EventEmitter } from 'eventemitter3';
import type { Agent } from 'node:http';
import type { Dispatcher } from 'undici';
import ioc, { createPreciseReader } from '../structure/io/binary/GraphBinary.js';
import GraphBinaryReader from '../structure/io/binary/internals/GraphBinaryReader.js';
import StreamReader from '../structure/io/binary/internals/StreamReader.js';
Expand All @@ -33,31 +33,58 @@ import {RequestMessage} from "./request-message.js";
import { HttpRequest, RequestInterceptor } from './http-request.js';
import ResponseError from './response-error.js';
import { Traverser } from '../process/traversal.js';
import { buildDispatcher } from './dispatcher.js';
import { Logger, LoggerCallback, normalizeLogger } from './logger.js';

const responseStatusCode = {
success: 200,
noContent: 204,
partialContent: 206,
};

/**
* Selects the content encoding requested from, and decoded for, the server. `'deflate'` (the
* default) sends an `Accept-Encoding: deflate` header and decodes deflate responses; `'none'`
* turns compression off.
*/
export type Compression = 'none' | 'deflate';

export type ConnectionOptions = {
ca?: string[];
cert?: string | string[] | Buffer;
pfx?: string | Buffer;
preciseNumbers?: boolean;
pdtRegistry?: any;
reader?: any;
rejectUnauthorized?: boolean;
traversalSource?: string;
headers?: Record<string, string | string[]>;
enableUserAgentOnConnect?: boolean;
agent?: Agent;
/** Maximum number of concurrent connections per origin. Defaults to 128. */
maxConnections?: number;
/** Idle-read (body) timeout in milliseconds, applied to the default dispatcher. */
readTimeoutMillis?: number;
/** Maximum size of the response headers in bytes, applied to the default dispatcher. */
maxResponseHeaderBytes?: number;
/**
* Idle time in milliseconds before TCP keep-alive probes begin on a connection. Defaults to
* 30000 (30s) when unset. Set to `0` to disable keep-alive entirely.
*/
keepAliveTimeMillis?: number;
/** HTTP proxy URI. When set, requests are routed through an undici `ProxyAgent`. */
proxy?: string;
/** Response compression codec. Defaults to `'deflate'` (on). */
compression?: Compression;
/** Connection-level default that fills a request's `batchSize` when it is left unset. Defaults to 64. */
batchSize?: number;
/** Connection-level default for `bulkResults`, applied to every request unless overridden per-request. Defaults to `false`. */
bulkResults?: boolean;
/** An optional logger (a logger object or a callback). Logging is disabled when unset. */
logger?: Logger;
interceptors?: RequestInterceptor | RequestInterceptor[];
/** An optional auth interceptor. As a convenience, this is always appended to the end of the
* interceptor list so it runs last, after any user interceptors have modified the request. */
auth?: RequestInterceptor;
};

/** The default per-request batch size used when neither the request nor the connection sets one. */
export const DEFAULT_BATCH_SIZE = 64;

/**
* Represents a single connection to a Gremlin Server.
*/
Expand All @@ -69,6 +96,11 @@ export default class Connection extends EventEmitter {

private readonly _enableUserAgentOnConnect: boolean;
private readonly _interceptors: RequestInterceptor[];
private readonly _dispatcher: Dispatcher | undefined;
private readonly _compression: Compression;
private readonly _batchSize: number;
private readonly _bulkResults: boolean;
private readonly _log: LoggerCallback;

/**
* Creates a new instance of {@link Connection}.
Expand All @@ -87,6 +119,29 @@ export default class Connection extends EventEmitter {
}
this.traversalSource = options.traversalSource || 'g';
this._enableUserAgentOnConnect = options.enableUserAgentOnConnect !== false;
this._log = normalizeLogger(options.logger);

if (options.compression === undefined) {
this._compression = 'deflate';
} else if (options.compression === 'none' || options.compression === 'deflate') {
this._compression = options.compression;
} else {
throw new TypeError(`compression must be 'none' or 'deflate'`);
}

this._batchSize = options.batchSize ?? DEFAULT_BATCH_SIZE;
this._bulkResults = options.bulkResults ?? false;

// The driver builds and owns the undici dispatcher from the discrete options.
// TLS is configured through the Node/undici runtime (e.g. NODE_EXTRA_CA_CERTS),
// not through a driver option.
this._dispatcher = buildDispatcher({
maxConnections: options.maxConnections,
readTimeoutMillis: options.readTimeoutMillis,
maxResponseHeaderBytes: options.maxResponseHeaderBytes,
keepAliveTimeMillis: options.keepAliveTimeMillis,
proxy: options.proxy,
});

const interceptors = options.interceptors;
if (typeof interceptors === 'function') {
Expand All @@ -103,6 +158,23 @@ export default class Connection extends EventEmitter {
if (options.auth) {
this._interceptors.push(options.auth);
}

this._log('debug', `Connection created for ${this.url}`);
}

/**
* The connection-level default batch size, used to fill a request's `batchSize` when unset.
*/
get batchSize(): number {
return this._batchSize;
}

/**
* The connection-level default for `bulkResults`, applied to every request unless overridden
* per-request. Defaults to `false`.
*/
get bulkResults(): boolean {
return this._bulkResults;
}

/**
Expand Down Expand Up @@ -178,7 +250,7 @@ export default class Connection extends EventEmitter {
}

if (!response.body) {
// 204 No Content nothing to yield
// 204 No Content - nothing to yield
return;
}

Expand All @@ -190,7 +262,7 @@ export default class Connection extends EventEmitter {
completed = true;
} finally {
if (!completed) {
// Consumer broke out early or an error occurred abort to release the connection
// Consumer broke out early or an error occurred - abort to release the connection
abortController.abort();
}
}
Expand All @@ -213,19 +285,17 @@ export default class Connection extends EventEmitter {
'Accept': this._reader.mimeType,
};

if (this._compression === 'deflate') {
headers['Accept-Encoding'] = 'deflate';
}

if (this._enableUserAgentOnConnect) {
const userAgent = await utils.getUserAgent();
if (userAgent !== undefined) {
headers[utils.getUserAgentHeader()] = userAgent;
}
}

if (this.options.headers) {
Object.entries(this.options.headers).forEach(([key, value]) => {
headers[key] = Array.isArray(value) ? value.join(', ') : value;
});
}

const httpRequest = new HttpRequest('POST', this.url, headers, request);

// Promote transactionId to HTTP header before interceptors run.
Expand All @@ -249,12 +319,17 @@ export default class Connection extends EventEmitter {
// Auto-serialize body to JSON after interceptors run (idempotent if already serialized)
httpRequest.serializeBody();

this._log('debug', `Sending ${httpRequest.method} request to ${httpRequest.url}`);

return fetch(httpRequest.url, {
method: httpRequest.method,
headers: httpRequest.headers,
body: httpRequest.body,
signal,
});
// Node only: the undici dispatcher carries the connection-pool options. In the browser
// it is undefined and the field is omitted, letting the user agent manage the transport.
...(this._dispatcher ? { dispatcher: this._dispatcher } : {}),
} as RequestInit);
}

async #handleResponse(response: Response) {
Expand Down Expand Up @@ -321,7 +396,8 @@ export default class Connection extends EventEmitter {
* Closes the Connection.
* @return {Promise}
*/
close() {
return Promise.resolve();
async close() {
this._log('debug', `Closing connection to ${this.url}`);
await this._dispatcher?.close();
}
}
Loading
Loading