From fe420b1b9d811857be562752653ed51cc599866a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Knut=20Olav=20L=C3=B8ite?= Date: Mon, 14 Sep 2026 17:29:12 +0200 Subject: [PATCH] perf(spanner): disable gRPC channelz by default @grpc/grpc-js has Channelz enabled by default ('grpc.enable_channelz': 1), which instruments every channel, subchannel, socket, and RPC attempt. On every call start and completion, Channelz updates call counters and allocates timestamps (`new Date()`) across both channel and transport layers, as well as maintaining trace ring buffers and child reference maps. Cloud Spanner telemetry is handled via OpenTelemetry, and the client does not expose or consume the gRPC Channelz admin service. This change sets 'grpc.enable_channelz': 0 by default in Spanner and GrpcService channel options, causing @grpc/grpc-js to use no-op stubs and eliminating per-RPC allocations. Users who require live connection introspection (e.g. via grpcdebug) can still explicitly pass 'grpc.enable_channelz': 1 in SpannerOptions to re-enable it. --- handwritten/spanner/src/index.ts | 27 +++++++++++++----- handwritten/spanner/test/index.ts | 18 ++++++++++++ handwritten/spanner/test/spanner.ts | 44 +++++++++++++++++++++++++++++ 3 files changed, 82 insertions(+), 7 deletions(-) diff --git a/handwritten/spanner/src/index.ts b/handwritten/spanner/src/index.ts index 17287fba98bf..a882114c91c9 100644 --- a/handwritten/spanner/src/index.ts +++ b/handwritten/spanner/src/index.ts @@ -155,6 +155,8 @@ export type GetInstanceConfigOperationsCallback = PagedCallback< * DirectedReadOptions won't be set for readWrite transactions" * @property {ObservabilityOptions} [observabilityOptions] Sets the observability options to be used for OpenTelemetry tracing * @property {boolean} [disableBuiltInMetrics=True] If set to true, built-in metrics will be disabled. + * @property {number} ['grpc.enable_channelz'=0] Whether to enable gRPC Channelz service tracking. + * Defaults to 0 (disabled) to eliminate per-RPC tracking and allocation overhead. Set to 1 to enable. */ export interface SpannerOptions extends GrpcClientOptions { apiEndpoint?: string; @@ -182,6 +184,12 @@ export interface SpannerOptions extends GrpcClientOptions { */ universe_domain?: string; universeDomain?: string; + /** + * Whether to enable gRPC Channelz service tracking. + * Defaults to `0` (disabled) to eliminate per-RPC allocation and tracking overhead. + * Set to `1` if live connection introspection via gRPC Channelz (e.g. grpcdebug) is required. + */ + 'grpc.enable_channelz'?: number; } export interface RequestConfig { client: string; @@ -423,6 +431,8 @@ class Spanner extends GrpcService { scopes, // Add grpc keep alive setting 'grpc.keepalive_time_ms': 120000, + // Disable Channelz by default to reduce per-RPC tracking and allocation overhead + 'grpc.enable_channelz': 0, // Enable grpc-gcp support 'grpc.callInvocationTransformer': grpcGcp.gcpCallInvocationTransformer, 'grpc.channelFactoryOverride': grpcGcp.gcpChannelFactoryOverride, @@ -534,7 +544,7 @@ class Spanner extends GrpcService { if (!this.clients_.has(clientName)) { this.clients_.set( clientName, - new v1[clientName](this.options as ClientOptions), + new v1.InstanceAdminClient(this.options as ClientOptions), ); } return this.clients_.get(clientName)! as v1.InstanceAdminClient; @@ -558,7 +568,7 @@ class Spanner extends GrpcService { if (!this.clients_.has(clientName)) { this.clients_.set( clientName, - new v1[clientName](this.options as ClientOptions), + new v1.DatabaseAdminClient(this.options as ClientOptions), ); } return this.clients_.get(clientName)! as v1.DatabaseAdminClient; @@ -615,10 +625,9 @@ class Spanner extends GrpcService { if (callback) { // process.nextTick prevents Unhandled Promise Rejections if callback throws - res.then( - () => process.nextTick(() => callback(null)), - err => process.nextTick(() => callback(err)), - ); + res + .then(() => process.nextTick(() => callback(null))) + .catch(err => process.nextTick(() => callback(err))); } else { return res; } @@ -1727,7 +1736,10 @@ class Spanner extends GrpcService { const clientName = config.client; try { if (!this.clients_.has(clientName)) { - this.clients_.set(clientName, new v1[clientName](this.options)); + this.clients_.set( + clientName, + new (v1 as Record)[clientName](this.options), + ); } } catch (err) { callback(err, null); @@ -1896,6 +1908,7 @@ class Spanner extends GrpcService { .then(val => { metricsTracer?.recordOperationCompletion(); resolve(val); + return val; }) .catch(error => { metricsTracer?.recordOperationCompletion(); diff --git a/handwritten/spanner/test/index.ts b/handwritten/spanner/test/index.ts index 6652d25fec63..c2d18e737674 100644 --- a/handwritten/spanner/test/index.ts +++ b/handwritten/spanner/test/index.ts @@ -230,6 +230,7 @@ describe('Spanner', () => { scopes: [], grpc, 'grpc.keepalive_time_ms': 120000, + 'grpc.enable_channelz': 0, 'grpc.callInvocationTransformer': fakeGrpcGcp().gcpCallInvocationTransformer, 'grpc.channelFactoryOverride': fakeGrpcGcp().gcpChannelFactoryOverride, @@ -295,6 +296,22 @@ describe('Spanner', () => { ); }); + it('should disable channelz by default and allow overriding it', () => { + const spannerDefault = new Spanner(OPTIONS); + assert.strictEqual( + (spannerDefault.options as any)['grpc.enable_channelz'], + 0, + ); + + const spannerEnabled = new Spanner( + Object.assign({}, OPTIONS, {'grpc.enable_channelz': 1}), + ); + assert.strictEqual( + (spannerEnabled.options as any)['grpc.enable_channelz'], + 1, + ); + }); + it('should inherit from GrpcService', () => { assert(spanner instanceof FakeGrpcService); @@ -2314,6 +2331,7 @@ describe('Spanner', () => { return spanner.request(CONFIG).then(result => { assert.strictEqual(result, gapicRequestFnResult); + return result; }); }); }); diff --git a/handwritten/spanner/test/spanner.ts b/handwritten/spanner/test/spanner.ts index 926aeb8ca63b..5c25c31bc4f3 100644 --- a/handwritten/spanner/test/spanner.ts +++ b/handwritten/spanner/test/spanner.ts @@ -30,6 +30,7 @@ import { Snapshot, Spanner, Transaction, + v1 as gapicV1, } from '../src'; import * as mock from './mockserver/mockspanner'; import { @@ -386,6 +387,49 @@ describe('Spanner with mock server', () => { assert.notStrictEqual(dbWithDefaultOptions, dbWithWriteSessions); }); + it('should disable channelz by default and allow overriding it', async () => { + assert.strictEqual((spanner.options as any)['grpc.enable_channelz'], 0); + + const client = new gapicV1.SpannerClient(spanner.options as any); + await client.initialize(); + const stub = (await (client as any).spannerStub) as any; + const channel = stub.getChannel(); + const pooledChannel = channel.channelRefs?.[0]?.channel as any; + assert.ok(pooledChannel, 'Expected pooledChannel to be initialized'); + assert.strictEqual(pooledChannel.internalChannel?.channelzEnabled, false); + + const customSpanner = new Spanner({ + servicePath: 'localhost', + port, + sslCreds: grpc.credentials.createInsecure(), + 'grpc.enable_channelz': 1, + }); + try { + assert.strictEqual( + (customSpanner.options as any)['grpc.enable_channelz'], + 1, + ); + const customClient = new gapicV1.SpannerClient( + customSpanner.options as any, + ); + await customClient.initialize(); + const customStub = (await (customClient as any).spannerStub) as any; + const customChannel = customStub.getChannel(); + const customPooledChannel = customChannel.channelRefs?.[0] + ?.channel as any; + assert.ok( + customPooledChannel, + 'Expected customPooledChannel to be initialized', + ); + assert.strictEqual( + customPooledChannel.internalChannel?.channelzEnabled, + true, + ); + } finally { + await customSpanner.close(); + } + }); + it('should execute query', async () => { // The query to execute const query = {