From 325476d22fabbbe7311b11e7fa53b631eeda784c Mon Sep 17 00:00:00 2001 From: Niraj Nepal Date: Thu, 13 Aug 2026 14:26:58 +0000 Subject: [PATCH 1/3] Fixed Resourcetimer and cache issue --- packages/grpc-js-xds/src/xds-client.ts | 13 ++++++++ packages/grpc-js-xds/test/test-core.ts | 44 +++++++++++++++++++++++++- 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/packages/grpc-js-xds/src/xds-client.ts b/packages/grpc-js-xds/src/xds-client.ts index 785c11943..e3c8947af 100644 --- a/packages/grpc-js-xds/src/xds-client.ts +++ b/packages/grpc-js-xds/src/xds-client.ts @@ -150,6 +150,7 @@ class ResourceTimer { if (!resourceState) { return; } + resourceState.cachedResource = null; resourceState.meta.clientStatus = 'DOES_NOT_EXIST'; for (const watcher of resourceState.watchers) { watcher.onResourceDoesNotExist(); @@ -372,6 +373,7 @@ class AdsCallState { experimental.log(logVerbosity.ERROR, 'Ignoring nonexistent resource ' + xdsResourceNameToString({authority, key}, result.type!.getTypeUrl())); resourceState.deletionIgnored = true; } else { + resourceState.cachedResource = null; resourceState.meta.clientStatus = 'DOES_NOT_EXIST'; process.nextTick(() => { for (const watcher of resourceState.watchers) { @@ -404,6 +406,13 @@ class AdsCallState { this.trace( 'ADS stream ended. code=' + streamStatus.code + ' details= ' + streamStatus.details ); + for (const typeState of this.typeStates.values()) { + for (const authorityMap of typeState.subscribedResources.values()) { + for (const timer of authorityMap.values()) { + timer.maybeCancelTimer(); + } + } + } if (streamStatus.code !== status.OK && !this.receivedAnyResponse) { for (const watcher of this.allWatchers()) { watcher.onError(streamStatus); @@ -458,6 +467,7 @@ class AdsCallState { if (!authorityMap) { return; } + authorityMap.get(name.key)?.maybeCancelTimer(); authorityMap.delete(name.key); if (authorityMap.size === 0) { typeState.subscribedResources.delete(name.authority); @@ -937,6 +947,9 @@ class XdsSingleServerClient { const metadata = new Metadata({waitForReady: true}); const call = this.adsClient.StreamAggregatedResources(metadata); this.adsCallState = new AdsCallState(this, call, this.xdsClient.adsNode!); + if (this.adsClient.getChannel().getConnectivityState(false) === connectivityState.READY) { + this.adsCallState.markStreamStarted(); + } this.adsBackoff.runOnce(); } diff --git a/packages/grpc-js-xds/test/test-core.ts b/packages/grpc-js-xds/test/test-core.ts index 3eef81dd9..523a28327 100644 --- a/packages/grpc-js-xds/test/test-core.ts +++ b/packages/grpc-js-xds/test/test-core.ts @@ -197,5 +197,47 @@ describe('core xDS functionality', () => { xdsServer.setRdsResource(routeGroup2.getRouteConfiguration()); await cluster2.waitForAllBackendsToReceiveTraffic(); client.stopCalls(); - }) + }); + it('should recover when a deleted LDS resource is restored with identical content', async () => { + const [backend] = await createBackends(1); + const serverRoute = new FakeServerRoute(backend.getPort(), 'serverRoute'); + xdsServer.setRdsResource(serverRoute.getRouteConfiguration()); + xdsServer.setLdsResource(serverRoute.getListener()); + xdsServer.addResponseListener((typeUrl, responseState) => { + if (responseState.state === 'NACKED') { + client?.stopCalls(); + assert.fail(`Client NACKED ${typeUrl} resource with message ${responseState.errorMessage}`); + } + }); + const cluster = new FakeEdsCluster('cluster1', 'endpoint1', [{backends: [backend], locality: {region: 'region1'}}]); + const routeGroup = new FakeRouteGroup('listener1', 'route1', [{cluster: cluster}]); + await routeGroup.startAllBackends(xdsServer); + xdsServer.setEdsResource(cluster.getEndpointConfig()); + xdsServer.setCdsResource(cluster.getClusterConfig()); + xdsServer.setRdsResource(routeGroup.getRouteConfiguration()); + xdsServer.setLdsResource(routeGroup.getListener()); + client = XdsTestClient.createFromServer('listener1', xdsServer); + client.startCalls(100); + await routeGroup.waitForAllBackendsToReceiveTraffic(); + client.stopCalls(); + + // Trigger transient LDS deletion (control plane flap) + xdsServer.unsetLdsResource('listener1'); + + // Verify call fails while resource is deleted + await new Promise((resolve) => { + client.sendOneCall((error) => { + assert(error, 'Expected RPC to fail after LDS deletion'); + resolve(); + }); + }); + + // Restore identical LDS resource + xdsServer.setLdsResource(routeGroup.getListener()); + + // Verify client recovers and traffic flows normally + client.startCalls(100); + await routeGroup.waitForAllBackendsToReceiveTraffic(); + client.stopCalls(); + }); }); From 30dfe0ff6884742d3838dccd254ca3a1c36445d3 Mon Sep 17 00:00:00 2001 From: Niraj Nepal Date: Thu, 27 Aug 2026 11:54:03 +0000 Subject: [PATCH 2/3] Fix test, cleaned code --- packages/grpc-js-xds/test/test-core.ts | 15 +++++++-------- packages/grpc-js-xds/test/xds-server.ts | 7 ++++++- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/packages/grpc-js-xds/test/test-core.ts b/packages/grpc-js-xds/test/test-core.ts index 523a28327..e6b369b48 100644 --- a/packages/grpc-js-xds/test/test-core.ts +++ b/packages/grpc-js-xds/test/test-core.ts @@ -221,16 +221,15 @@ describe('core xDS functionality', () => { await routeGroup.waitForAllBackendsToReceiveTraffic(); client.stopCalls(); - // Trigger transient LDS deletion (control plane flap) xdsServer.unsetLdsResource('listener1'); - // Verify call fails while resource is deleted - await new Promise((resolve) => { - client.sendOneCall((error) => { - assert(error, 'Expected RPC to fail after LDS deletion'); - resolve(); - }); - }); + const deadline = Date.now() + 1000; + while (client.getConnectivityState() === connectivityState.READY && Date.now() < deadline) { + await new Promise(resolve => setTimeout(resolve, 50)); + } + + const error = await client.sendOneCallAsync(); + assert(error, 'Expected RPC to fail after LDS deletion'); // Restore identical LDS resource xdsServer.setLdsResource(routeGroup.getListener()); diff --git a/packages/grpc-js-xds/test/xds-server.ts b/packages/grpc-js-xds/test/xds-server.ts index d6115e79a..e3eb48066 100644 --- a/packages/grpc-js-xds/test/xds-server.ts +++ b/packages/grpc-js-xds/test/xds-server.ts @@ -177,6 +177,11 @@ export class ControlPlaneServer { this.setResource({...resource, '@type': LDS_TYPE_URL}, resource.name!); } + unsetLdsResource(name: string) { + trace(`unsetLdsResource(${name})`); + this.unsetResource(LDS_TYPE_URL, name); + } + setRdsResource(resource: RouteConfiguration) { trace(`setRdsResource(${resource.name!})`); this.setResource({...resource, '@type': RDS_TYPE_URL}, resource.name!); @@ -209,7 +214,7 @@ export class ControlPlaneServer { private sendResourceUpdates(typeUrl: T, clients: Set, includeResources: Set) { const resourceTypeState = this.resourceMap[typeUrl] as ResourceTypeState; - const clientResources = new Map(); + const clientResources = new Map(Array.from(clients, client => [client, []])); for (const [resourceName, resourceState] of resourceTypeState.resourceNameMap) { /* For RDS and EDS, only send updates for the listed updated resources. * Otherwise include all resources. */ From 211d2dda8eee3f8bcf973d51b701f8d59eca1734 Mon Sep 17 00:00:00 2001 From: Niraj Nepal Date: Fri, 28 Aug 2026 12:56:38 +0000 Subject: [PATCH 3/3] Addressed review comments --- packages/grpc-js-xds/src/xds-client.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/grpc-js-xds/src/xds-client.ts b/packages/grpc-js-xds/src/xds-client.ts index e3c8947af..3c496e406 100644 --- a/packages/grpc-js-xds/src/xds-client.ts +++ b/packages/grpc-js-xds/src/xds-client.ts @@ -150,7 +150,9 @@ class ResourceTimer { if (!resourceState) { return; } - resourceState.cachedResource = null; + if (resourceState.cachedResource !== null) { + return; + } resourceState.meta.clientStatus = 'DOES_NOT_EXIST'; for (const watcher of resourceState.watchers) { watcher.onResourceDoesNotExist();