diff --git a/packages/grpc-js-xds/package.json b/packages/grpc-js-xds/package.json index a55c631cf..606bd9faf 100644 --- a/packages/grpc-js-xds/package.json +++ b/packages/grpc-js-xds/package.json @@ -1,6 +1,6 @@ { "name": "@grpc/grpc-js-xds", - "version": "1.9.0", + "version": "1.9.1", "description": "Plugin for @grpc/grpc-js. Adds the xds:// URL scheme and associated features.", "main": "build/src/index.js", "scripts": { diff --git a/packages/grpc-js-xds/src/resolver-xds.ts b/packages/grpc-js-xds/src/resolver-xds.ts index a934e7285..17d3c6305 100644 --- a/packages/grpc-js-xds/src/resolver-xds.ts +++ b/packages/grpc-js-xds/src/resolver-xds.ts @@ -213,7 +213,7 @@ function getPredicateForMatcher(routeMatch: RouteMatch__Output): Matcher { * the ServiceConfig definition. The difference is that the protobuf message * defines seconds as a long, which is represented as a string in JavaScript, * and the one used in the service config defines it as a number. - * @param duration + * @param duration */ function protoDurationToDuration(duration: Duration__Output): Duration { return { @@ -235,7 +235,7 @@ function getDefaultRetryMaxInterval(baseInterval: string): string { /** * Encode a text string as a valid path of a URI, as specified in RFC-3986 section 3.3 * @param uriPath A value representing an unencoded URI path - * @returns + * @returns */ function encodeURIPath(uriPath: string): string { return uriPath.replace(/[^A-Za-z0-9._~!$&^()*+,;=/-]/g, substring => encodeURIComponent(substring)); @@ -447,7 +447,7 @@ class XdsResolver implements Resolver { } } } - let retryPolicy: RetryPolicy | undefined = undefined; + let retryPolicy: RetryPolicy | undefined = undefined; if (EXPERIMENTAL_RETRY) { const retryConfig = route.route!.retry_policy ?? virtualHost.retry_policy; if (retryConfig) { @@ -458,10 +458,10 @@ class XdsResolver implements Resolver { } } if (retryableStatusCodes.length > 0) { - const baseInterval = retryConfig.retry_back_off?.base_interval ? - protoDurationToSecondsString(retryConfig.retry_back_off.base_interval) : + const baseInterval = retryConfig.retry_back_off?.base_interval ? + protoDurationToSecondsString(retryConfig.retry_back_off.base_interval) : DEFAULT_RETRY_BASE_INTERVAL; - const maxInterval = retryConfig.retry_back_off?.max_interval ? + const maxInterval = retryConfig.retry_back_off?.max_interval ? protoDurationToSecondsString(retryConfig.retry_back_off.max_interval) : getDefaultRetryMaxInterval(baseInterval); retryPolicy = { @@ -664,9 +664,11 @@ class XdsResolver implements Resolver { destroy() { if (this.listenerResourceName) { ListenerResourceType.cancelWatch(this.xdsClient, this.listenerResourceName, this.ldsWatcher); + this.isLdsWatcherActive = false; } if (this.latestRouteConfigName) { RouteConfigurationResourceType.cancelWatch(this.xdsClient, this.latestRouteConfigName, this.rdsWatcher); + this.latestRouteConfigName = null; } } diff --git a/packages/grpc-js-xds/test/client.ts b/packages/grpc-js-xds/test/client.ts index 6d346f918..0779702bb 100644 --- a/packages/grpc-js-xds/test/client.ts +++ b/packages/grpc-js-xds/test/client.ts @@ -15,7 +15,7 @@ * */ -import { credentials, loadPackageDefinition, ServiceError } from "@grpc/grpc-js"; +import { ChannelOptions, credentials, loadPackageDefinition, ServiceError } from "@grpc/grpc-js"; import { loadSync } from "@grpc/proto-loader"; import { ProtoGrpcType } from "./generated/echo"; import { EchoTestServiceClient } from "./generated/grpc/testing/EchoTestService"; @@ -44,14 +44,14 @@ export class XdsTestClient { private client: EchoTestServiceClient; private callInterval: NodeJS.Timer; - constructor(target: string, bootstrapInfo: string) { - this.client = new loadedProtos.grpc.testing.EchoTestService(target, credentials.createInsecure(), {[BOOTSTRAP_CONFIG_KEY]: bootstrapInfo}); + constructor(target: string, bootstrapInfo: string, options?: ChannelOptions) { + this.client = new loadedProtos.grpc.testing.EchoTestService(target, credentials.createInsecure(), {...options, [BOOTSTRAP_CONFIG_KEY]: bootstrapInfo}); this.callInterval = setInterval(() => {}, 0); clearInterval(this.callInterval); } - static createFromServer(targetName: string, xdsServer: XdsServer) { - return new XdsTestClient(`xds:///${targetName}`, xdsServer.getBootstrapInfoString()); + static createFromServer(targetName: string, xdsServer: XdsServer, options?: ChannelOptions) { + return new XdsTestClient(`xds:///${targetName}`, xdsServer.getBootstrapInfoString(), options); } startCalls(interval: number) { @@ -98,4 +98,8 @@ export class XdsTestClient { } sendInner(count, callback); } + + getConnectivityState() { + return this.client.getChannel().getConnectivityState(false); + } } diff --git a/packages/grpc-js-xds/test/test-core.ts b/packages/grpc-js-xds/test/test-core.ts index cb145eb81..f48ab6c11 100644 --- a/packages/grpc-js-xds/test/test-core.ts +++ b/packages/grpc-js-xds/test/test-core.ts @@ -22,6 +22,7 @@ import { XdsServer } from "./xds-server"; import { register } from "../src"; import assert = require("assert"); +import { connectivityState } from "@grpc/grpc-js"; register(); @@ -60,4 +61,34 @@ describe('core xDS functionality', () => { }, reason => done(reason)); }, reason => done(reason)); }); + it('should be able to enter and exit idle', function(done) { + this.timeout(5000); + const cluster = new FakeEdsCluster('cluster1', 'endpoint1', [{backends: [new Backend()], locality:{region: 'region1'}}]); + const routeGroup = new FakeRouteGroup('listener1', 'route1', [{cluster: cluster}]); + routeGroup.startAllBackends().then(() => { + xdsServer.setEdsResource(cluster.getEndpointConfig()); + xdsServer.setCdsResource(cluster.getClusterConfig()); + xdsServer.setRdsResource(routeGroup.getRouteConfiguration()); + xdsServer.setLdsResource(routeGroup.getListener()); + xdsServer.addResponseListener((typeUrl, responseState) => { + if (responseState.state === 'NACKED') { + client.stopCalls(); + assert.fail(`Client NACKED ${typeUrl} resource with message ${responseState.errorMessage}`); + } + }) + client = XdsTestClient.createFromServer('listener1', xdsServer, { + 'grpc.client_idle_timeout_ms': 1000, + }); + client.sendOneCall(error => { + assert.ifError(error); + assert.strictEqual(client.getConnectivityState(), connectivityState.READY); + setTimeout(() => { + assert.strictEqual(client.getConnectivityState(), connectivityState.IDLE); + client.sendOneCall(error => { + done(error); + }) + }, 1100); + }); + }, reason => done(reason)); + }); });