Skip to content

Commit 5a942ed

Browse files
authored
Merge pull request #2883 from murgatroid99/grpc-js-xds_config_selector_cluster_ref
grpc-js-xds: Reference clusters for ConfigSelector lifetime
2 parents 22bbe8a + bfd87a9 commit 5a942ed

File tree

4 files changed

+124
-66
lines changed

4 files changed

+124
-66
lines changed

packages/grpc-js-xds/src/resolver-xds.ts

+79-29
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,30 @@ const RETRY_CODES: {[key: string]: status} = {
8383
export const XDS_CONFIG_KEY = `${experimental.SUBCHANNEL_ARGS_EXCLUDE_KEY_PREFIX}.xds_config`;
8484
export const XDS_CLIENT_KEY = 'grpc.internal.xds_client';
8585

86+
/**
87+
* Tracks a dynamic subscription to a cluster that is currently or previously
88+
* referenced in a RouteConfiguration.
89+
*/
90+
class ClusterRef {
91+
private refCount = 0;
92+
constructor(private unsubscribe: () => void) {}
93+
94+
ref() {
95+
this.refCount += 1;
96+
}
97+
98+
unref() {
99+
this.refCount -= 1;
100+
if (this.refCount <= 0) {
101+
this.unsubscribe();
102+
}
103+
}
104+
105+
hasRef() {
106+
return this.refCount > 0;
107+
}
108+
}
109+
86110
class XdsResolver implements Resolver {
87111

88112
private listenerResourceName: string | null = null;
@@ -93,6 +117,7 @@ class XdsResolver implements Resolver {
93117

94118
private xdsConfigWatcher: XdsConfigWatcher;
95119
private xdsDependencyManager: XdsDependencyManager | null = null;
120+
private clusterRefs: Map<string, ClusterRef> = new Map();
96121

97122
constructor(
98123
private target: GrpcUri,
@@ -123,11 +148,20 @@ class XdsResolver implements Resolver {
123148
}
124149
}
125150

151+
private pruneUnusedClusters() {
152+
for (const [cluster, clusterRef] of this.clusterRefs) {
153+
if (!clusterRef.hasRef()) {
154+
this.clusterRefs.delete(cluster);
155+
}
156+
}
157+
}
158+
126159
private async handleXdsConfig(xdsConfig: XdsConfig) {
127160
/* We need to load the xxhash API before this function finishes, because
128161
* it is invoked in the config selector, which can be called immediately
129162
* after this function returns. */
130163
await loadXxhashApi();
164+
this.pruneUnusedClusters();
131165
const httpConnectionManager = decodeSingleResource(HTTP_CONNECTION_MANGER_TYPE_URL, xdsConfig.listener.api_listener!.api_listener!.value);
132166
const configDefaultTimeout = httpConnectionManager.common_http_protocol_options?.idle_timeout;
133167
let defaultTimeout: Duration | undefined = undefined;
@@ -312,44 +346,60 @@ class XdsResolver implements Resolver {
312346
const routeMatcher = getPredicateForMatcher(route.match!);
313347
matchList.push({matcher: routeMatcher, action: routeAction});
314348
}
315-
const configSelector: ConfigSelector = (methodName, metadata, channelId) => {
316-
for (const {matcher, action} of matchList) {
317-
if (matcher.apply(methodName, metadata)) {
318-
const clusterResult = action.getCluster();
319-
const unrefCluster = this.xdsDependencyManager!.addClusterSubscription(clusterResult.name);
320-
const onCommitted = () => {
321-
unrefCluster();
322-
}
323-
let hash: string;
324-
if (EXPERIMENTAL_RING_HASH) {
325-
hash = `${action.getHash(metadata, channelId)}`;
326-
} else {
327-
hash = '';
349+
for (const cluster of allConfigClusters) {
350+
let clusterRef = this.clusterRefs.get(cluster);
351+
if (!clusterRef) {
352+
clusterRef = new ClusterRef(this.xdsDependencyManager!.addClusterSubscription(cluster));
353+
this.clusterRefs.set(cluster, clusterRef);
354+
}
355+
clusterRef.ref();
356+
}
357+
const configSelector: ConfigSelector = {
358+
invoke: (methodName, metadata, channelId) => {
359+
for (const {matcher, action} of matchList) {
360+
if (matcher.apply(methodName, metadata)) {
361+
const clusterResult = action.getCluster();
362+
const clusterRef = this.clusterRefs.get(clusterResult.name)!;
363+
clusterRef.ref();
364+
const onCommitted = () => {
365+
clusterRef.unref();
366+
}
367+
let hash: string;
368+
if (EXPERIMENTAL_RING_HASH) {
369+
hash = `${action.getHash(metadata, channelId)}`;
370+
} else {
371+
hash = '';
372+
}
373+
return {
374+
methodConfig: clusterResult.methodConfig,
375+
onCommitted: onCommitted,
376+
pickInformation: {cluster: clusterResult.name, hash: hash},
377+
status: status.OK,
378+
dynamicFilterFactories: clusterResult.dynamicFilterFactories
379+
};
328380
}
329-
return {
330-
methodConfig: clusterResult.methodConfig,
331-
onCommitted: onCommitted,
332-
pickInformation: {cluster: clusterResult.name, hash: hash},
333-
status: status.OK,
334-
dynamicFilterFactories: clusterResult.dynamicFilterFactories
335-
};
381+
}
382+
return {
383+
methodConfig: {name: []},
384+
// These fields won't be used here, but they're set because of some TypeScript weirdness
385+
pickInformation: {cluster: '', hash: ''},
386+
status: status.UNAVAILABLE,
387+
dynamicFilterFactories: []
388+
};
389+
},
390+
unref: () => {
391+
for (const cluster of allConfigClusters) {
392+
this.clusterRefs.get(cluster)?.unref();
336393
}
337394
}
338-
return {
339-
methodConfig: {name: []},
340-
// These fields won't be used here, but they're set because of some TypeScript weirdness
341-
pickInformation: {cluster: '', hash: ''},
342-
status: status.UNAVAILABLE,
343-
dynamicFilterFactories: []
344-
};
345-
};
395+
}
346396
trace('Created ConfigSelector with configuration:');
347397
for (const {matcher, action} of matchList) {
348398
trace(matcher.toString());
349399
trace('=> ' + action.toString());
350400
}
351401
const clusterConfigMap: {[key: string]: {child_policy: LoadBalancingConfig[]}} = {};
352-
for (const clusterName of allConfigClusters) {
402+
for (const clusterName of this.clusterRefs.keys()) {
353403
clusterConfigMap[clusterName] = {child_policy: [{cds: {cluster: clusterName}}]};
354404
}
355405
const lbPolicyConfig = {xds_cluster_manager: {children: clusterConfigMap}};

packages/grpc-js/src/internal-channel.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -377,6 +377,7 @@ export class InternalChannel {
377377
'Address resolution succeeded'
378378
);
379379
}
380+
this.configSelector?.unref();
380381
this.configSelector = configSelector;
381382
this.currentResolutionError = null;
382383
/* We process the queue asynchronously to ensure that the corresponding
@@ -568,7 +569,7 @@ export class InternalChannel {
568569
if (this.configSelector) {
569570
return {
570571
type: 'SUCCESS',
571-
config: this.configSelector(method, metadata, this.randomChannelId),
572+
config: this.configSelector.invoke(method, metadata, this.randomChannelId),
572573
};
573574
} else {
574575
if (this.currentResolutionError) {
@@ -790,6 +791,8 @@ export class InternalChannel {
790791
}
791792

792793
this.subchannelPool.unrefUnusedSubchannels();
794+
this.configSelector?.unref();
795+
this.configSelector = null;
793796
}
794797

795798
getTarget() {

packages/grpc-js/src/resolver.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@ export interface CallConfig {
3737
* https://github.com/grpc/proposal/blob/master/A31-xds-timeout-support-and-config-selector.md#new-functionality-in-grpc
3838
*/
3939
export interface ConfigSelector {
40-
(methodName: string, metadata: Metadata, channelId: number): CallConfig;
40+
invoke(methodName: string, metadata: Metadata, channelId: number): CallConfig;
41+
unref(): void;
4142
}
4243

4344
/**

packages/grpc-js/src/resolving-load-balancer.ts

+39-35
Original file line numberDiff line numberDiff line change
@@ -103,43 +103,46 @@ function findMatchingConfig(
103103
function getDefaultConfigSelector(
104104
serviceConfig: ServiceConfig | null
105105
): ConfigSelector {
106-
return function defaultConfigSelector(
107-
methodName: string,
108-
metadata: Metadata
109-
) {
110-
const splitName = methodName.split('/').filter(x => x.length > 0);
111-
const service = splitName[0] ?? '';
112-
const method = splitName[1] ?? '';
113-
if (serviceConfig && serviceConfig.methodConfig) {
114-
/* Check for the following in order, and return the first method
115-
* config that matches:
116-
* 1. A name that exactly matches the service and method
117-
* 2. A name with no method set that matches the service
118-
* 3. An empty name
119-
*/
120-
for (const matchLevel of NAME_MATCH_LEVEL_ORDER) {
121-
const matchingConfig = findMatchingConfig(
122-
service,
123-
method,
124-
serviceConfig.methodConfig,
125-
matchLevel
126-
);
127-
if (matchingConfig) {
128-
return {
129-
methodConfig: matchingConfig,
130-
pickInformation: {},
131-
status: Status.OK,
132-
dynamicFilterFactories: [],
133-
};
106+
return {
107+
invoke(
108+
methodName: string,
109+
metadata: Metadata
110+
) {
111+
const splitName = methodName.split('/').filter(x => x.length > 0);
112+
const service = splitName[0] ?? '';
113+
const method = splitName[1] ?? '';
114+
if (serviceConfig && serviceConfig.methodConfig) {
115+
/* Check for the following in order, and return the first method
116+
* config that matches:
117+
* 1. A name that exactly matches the service and method
118+
* 2. A name with no method set that matches the service
119+
* 3. An empty name
120+
*/
121+
for (const matchLevel of NAME_MATCH_LEVEL_ORDER) {
122+
const matchingConfig = findMatchingConfig(
123+
service,
124+
method,
125+
serviceConfig.methodConfig,
126+
matchLevel
127+
);
128+
if (matchingConfig) {
129+
return {
130+
methodConfig: matchingConfig,
131+
pickInformation: {},
132+
status: Status.OK,
133+
dynamicFilterFactories: [],
134+
};
135+
}
134136
}
135137
}
136-
}
137-
return {
138-
methodConfig: { name: [] },
139-
pickInformation: {},
140-
status: Status.OK,
141-
dynamicFilterFactories: [],
142-
};
138+
return {
139+
methodConfig: { name: [] },
140+
pickInformation: {},
141+
status: Status.OK,
142+
dynamicFilterFactories: [],
143+
};
144+
},
145+
unref() {}
143146
};
144147
}
145148

@@ -298,6 +301,7 @@ export class ResolvingLoadBalancer implements LoadBalancer {
298301
'All load balancer options in service config are not compatible',
299302
metadata: new Metadata(),
300303
});
304+
configSelector?.unref();
301305
return;
302306
}
303307
this.childLoadBalancer.updateAddressList(

0 commit comments

Comments
 (0)