grpc-js: Fix method config name handling in service configs

This commit is contained in:
Michael Lumish 2023-08-21 11:14:17 -07:00
parent 1df90db060
commit 69257a7893
3 changed files with 108 additions and 28 deletions

View File

@ -1,6 +1,6 @@
{ {
"name": "@grpc/grpc-js", "name": "@grpc/grpc-js",
"version": "1.9.0", "version": "1.9.1",
"description": "gRPC Library for Node - pure JS implementation", "description": "gRPC Library for Node - pure JS implementation",
"homepage": "https://grpc.io/", "homepage": "https://grpc.io/",
"repository": "https://github.com/grpc/grpc-node/tree/master/packages/grpc-js", "repository": "https://github.com/grpc/grpc-node/tree/master/packages/grpc-js",

View File

@ -21,7 +21,11 @@ import {
LoadBalancingConfig, LoadBalancingConfig,
getFirstUsableConfig, getFirstUsableConfig,
} from './load-balancer'; } from './load-balancer';
import { ServiceConfig, validateServiceConfig } from './service-config'; import {
MethodConfig,
ServiceConfig,
validateServiceConfig,
} from './service-config';
import { ConnectivityState } from './connectivity-state'; import { ConnectivityState } from './connectivity-state';
import { ConfigSelector, createResolver, Resolver } from './resolver'; import { ConfigSelector, createResolver, Resolver } from './resolver';
import { ServiceError } from './call'; import { ServiceError } from './call';
@ -43,6 +47,59 @@ function trace(text: string): void {
logging.trace(LogVerbosity.DEBUG, TRACER_NAME, text); logging.trace(LogVerbosity.DEBUG, TRACER_NAME, text);
} }
type NameMatchLevel = 'EMPTY' | 'SERVICE' | 'SERVICE_AND_METHOD';
/**
* Name match levels in order from most to least specific. This is the order in
* which searches will be performed.
*/
const NAME_MATCH_LEVEL_ORDER: NameMatchLevel[] = [
'SERVICE_AND_METHOD',
'SERVICE',
'EMPTY',
];
function hasMatchingName(
service: string,
method: string,
methodConfig: MethodConfig,
matchLevel: NameMatchLevel
): boolean {
for (const name of methodConfig.name) {
switch (matchLevel) {
case 'EMPTY':
if (!name.service && !name.method) {
return true;
}
break;
case 'SERVICE':
if (name.service === service && !name.method) {
return true;
}
break;
case 'SERVICE_AND_METHOD':
if (name.service === service && name.method === method) {
return true;
}
}
}
return false;
}
function findMatchingConfig(
service: string,
method: string,
methodConfigs: MethodConfig[],
matchLevel: NameMatchLevel
): MethodConfig | null {
for (const config of methodConfigs) {
if (hasMatchingName(service, method, config, matchLevel)) {
return config;
}
}
return null;
}
function getDefaultConfigSelector( function getDefaultConfigSelector(
serviceConfig: ServiceConfig | null serviceConfig: ServiceConfig | null
): ConfigSelector { ): ConfigSelector {
@ -54,14 +111,22 @@ function getDefaultConfigSelector(
const service = splitName[0] ?? ''; const service = splitName[0] ?? '';
const method = splitName[1] ?? ''; const method = splitName[1] ?? '';
if (serviceConfig && serviceConfig.methodConfig) { if (serviceConfig && serviceConfig.methodConfig) {
for (const methodConfig of serviceConfig.methodConfig) { /* Check for the following in order, and return the first method
for (const name of methodConfig.name) { * config that matches:
if ( * 1. A name that exactly matches the service and method
name.service === service && * 2. A name with no method set that matches the service
(name.method === undefined || name.method === method) * 3. An empty name
) { */
for (const matchLevel of NAME_MATCH_LEVEL_ORDER) {
const matchingConfig = findMatchingConfig(
service,
method,
serviceConfig.methodConfig,
matchLevel
);
if (matchingConfig) {
return { return {
methodConfig: methodConfig, methodConfig: matchingConfig,
pickInformation: {}, pickInformation: {},
status: Status.OK, status: Status.OK,
dynamicFilterFactories: [], dynamicFilterFactories: [],
@ -69,7 +134,6 @@ function getDefaultConfigSelector(
} }
} }
} }
}
return { return {
methodConfig: { name: [] }, methodConfig: { name: [] },
pickInformation: {}, pickInformation: {},

View File

@ -35,7 +35,7 @@ import {
} from './load-balancer'; } from './load-balancer';
export interface MethodConfigName { export interface MethodConfigName {
service: string; service?: string;
method?: string; method?: string;
} }
@ -95,20 +95,36 @@ const DURATION_REGEX = /^\d+(\.\d{1,9})?s$/;
const CLIENT_LANGUAGE_STRING = 'node'; const CLIENT_LANGUAGE_STRING = 'node';
function validateName(obj: any): MethodConfigName { function validateName(obj: any): MethodConfigName {
if (!('service' in obj) || typeof obj.service !== 'string') { // In this context, and unset field and '' are considered the same
throw new Error('Invalid method config name: invalid service'); if ('service' in obj && obj.service !== '') {
if (typeof obj.service !== 'string') {
throw new Error(
`Invalid method config name: invalid service: expected type string, got ${typeof obj.service}`
);
} }
const result: MethodConfigName = { if ('method' in obj && obj.method !== '') {
if (typeof obj.method !== 'string') {
throw new Error(
`Invalid method config name: invalid method: expected type string, got ${typeof obj.service}`
);
}
return {
service: obj.service,
method: obj.method,
};
} else {
return {
service: obj.service, service: obj.service,
}; };
if ('method' in obj) { }
if (typeof obj.method === 'string') {
result.method = obj.method;
} else { } else {
throw new Error('Invalid method config name: invalid method'); if ('method' in obj && obj.method !== undefined) {
throw new Error(
`Invalid method config name: method set with empty or unset service`
);
} }
return {};
} }
return result;
} }
function validateRetryPolicy(obj: any): RetryPolicy { function validateRetryPolicy(obj: any): RetryPolicy {