Skip to content

Commit 7ca0af6

Browse files
authored
Merge pull request #2550 from murgatroid99/grpc-js_service_config_parsing
grpc-js: Fix method config name handling in service configs
2 parents cd25bad + 69257a7 commit 7ca0af6

File tree

3 files changed

+107
-27
lines changed

3 files changed

+107
-27
lines changed

packages/grpc-js/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@grpc/grpc-js",
3-
"version": "1.9.0",
3+
"version": "1.9.1",
44
"description": "gRPC Library for Node - pure JS implementation",
55
"homepage": "https://grpc.io/",
66
"repository": "https://github.com/grpc/grpc-node/tree/master/packages/grpc-js",

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

Lines changed: 78 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,11 @@ import {
2121
LoadBalancingConfig,
2222
getFirstUsableConfig,
2323
} from './load-balancer';
24-
import { ServiceConfig, validateServiceConfig } from './service-config';
24+
import {
25+
MethodConfig,
26+
ServiceConfig,
27+
validateServiceConfig,
28+
} from './service-config';
2529
import { ConnectivityState } from './connectivity-state';
2630
import { ConfigSelector, createResolver, Resolver } from './resolver';
2731
import { ServiceError } from './call';
@@ -43,6 +47,59 @@ function trace(text: string): void {
4347
logging.trace(LogVerbosity.DEBUG, TRACER_NAME, text);
4448
}
4549

50+
type NameMatchLevel = 'EMPTY' | 'SERVICE' | 'SERVICE_AND_METHOD';
51+
52+
/**
53+
* Name match levels in order from most to least specific. This is the order in
54+
* which searches will be performed.
55+
*/
56+
const NAME_MATCH_LEVEL_ORDER: NameMatchLevel[] = [
57+
'SERVICE_AND_METHOD',
58+
'SERVICE',
59+
'EMPTY',
60+
];
61+
62+
function hasMatchingName(
63+
service: string,
64+
method: string,
65+
methodConfig: MethodConfig,
66+
matchLevel: NameMatchLevel
67+
): boolean {
68+
for (const name of methodConfig.name) {
69+
switch (matchLevel) {
70+
case 'EMPTY':
71+
if (!name.service && !name.method) {
72+
return true;
73+
}
74+
break;
75+
case 'SERVICE':
76+
if (name.service === service && !name.method) {
77+
return true;
78+
}
79+
break;
80+
case 'SERVICE_AND_METHOD':
81+
if (name.service === service && name.method === method) {
82+
return true;
83+
}
84+
}
85+
}
86+
return false;
87+
}
88+
89+
function findMatchingConfig(
90+
service: string,
91+
method: string,
92+
methodConfigs: MethodConfig[],
93+
matchLevel: NameMatchLevel
94+
): MethodConfig | null {
95+
for (const config of methodConfigs) {
96+
if (hasMatchingName(service, method, config, matchLevel)) {
97+
return config;
98+
}
99+
}
100+
return null;
101+
}
102+
46103
function getDefaultConfigSelector(
47104
serviceConfig: ServiceConfig | null
48105
): ConfigSelector {
@@ -54,19 +111,26 @@ function getDefaultConfigSelector(
54111
const service = splitName[0] ?? '';
55112
const method = splitName[1] ?? '';
56113
if (serviceConfig && serviceConfig.methodConfig) {
57-
for (const methodConfig of serviceConfig.methodConfig) {
58-
for (const name of methodConfig.name) {
59-
if (
60-
name.service === service &&
61-
(name.method === undefined || name.method === method)
62-
) {
63-
return {
64-
methodConfig: methodConfig,
65-
pickInformation: {},
66-
status: Status.OK,
67-
dynamicFilterFactories: [],
68-
};
69-
}
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+
};
70134
}
71135
}
72136
}

packages/grpc-js/src/service-config.ts

Lines changed: 28 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ import {
3535
} from './load-balancer';
3636

3737
export interface MethodConfigName {
38-
service: string;
38+
service?: string;
3939
method?: string;
4040
}
4141

@@ -95,20 +95,36 @@ const DURATION_REGEX = /^\d+(\.\d{1,9})?s$/;
9595
const CLIENT_LANGUAGE_STRING = 'node';
9696

9797
function validateName(obj: any): MethodConfigName {
98-
if (!('service' in obj) || typeof obj.service !== 'string') {
99-
throw new Error('Invalid method config name: invalid service');
100-
}
101-
const result: MethodConfigName = {
102-
service: obj.service,
103-
};
104-
if ('method' in obj) {
105-
if (typeof obj.method === 'string') {
106-
result.method = obj.method;
98+
// In this context, and unset field and '' are considered the same
99+
if ('service' in obj && obj.service !== '') {
100+
if (typeof obj.service !== 'string') {
101+
throw new Error(
102+
`Invalid method config name: invalid service: expected type string, got ${typeof obj.service}`
103+
);
104+
}
105+
if ('method' in obj && obj.method !== '') {
106+
if (typeof obj.method !== 'string') {
107+
throw new Error(
108+
`Invalid method config name: invalid method: expected type string, got ${typeof obj.service}`
109+
);
110+
}
111+
return {
112+
service: obj.service,
113+
method: obj.method,
114+
};
107115
} else {
108-
throw new Error('Invalid method config name: invalid method');
116+
return {
117+
service: obj.service,
118+
};
109119
}
120+
} else {
121+
if ('method' in obj && obj.method !== undefined) {
122+
throw new Error(
123+
`Invalid method config name: method set with empty or unset service`
124+
);
125+
}
126+
return {};
110127
}
111-
return result;
112128
}
113129

114130
function validateRetryPolicy(obj: any): RetryPolicy {

0 commit comments

Comments
 (0)